diff --git a/myproject/orders/services/__init__.py b/myproject/orders/services/__init__.py
index 9454d77..b9f0969 100644
--- a/myproject/orders/services/__init__.py
+++ b/myproject/orders/services/__init__.py
@@ -1,7 +1,5 @@
"""
-!5@28A=K9 A;>9 4;O ?@8;>65=8O orders.
+Сервисный слой для приложения orders.
"""
-from .draft_service import DraftOrderService
-
-__all__ = ['DraftOrderService']
+__all__ = []
diff --git a/myproject/orders/services/draft_service.py b/myproject/orders/services/draft_service.py
deleted file mode 100644
index 28547e2..0000000
--- a/myproject/orders/services/draft_service.py
+++ /dev/null
@@ -1,564 +0,0 @@
-"""
-Сервис для работы с черновиками заказов.
-Содержит бизнес-логику создания, обновления и завершения черновиков.
-"""
-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_warehouse=data.get('pickup_warehouse'),
- 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')),
- 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_warehouse': 'inventory.Warehouse',
- 'status': 'orders.OrderStatus',
- }
-
- simple_fields = [
- 'is_delivery', 'delivery_date', 'delivery_time_start', 'delivery_time_end',
- 'delivery_cost', '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)
-
- # Обрабатываем удаление позиций заказа
- if 'deleted_item_ids' in data:
- deleted_ids = data['deleted_item_ids']
- if deleted_ids:
- from ..models import OrderItem
- OrderItem.objects.filter(id__in=deleted_ids, order=order).delete()
-
- # Обрабатываем позиции заказа (items)
- if 'items' in data:
- # Импортируем модели
- from products.models import Product, ProductKit
- from ..models import OrderItem
-
- items_data = data['items']
-
- # Обрабатываем каждую позицию
- for item_data in items_data:
- item_id = item_data.get('id') # ID существующей позиции (если есть)
- product_id = item_data.get('product_id')
- product_kit_id = item_data.get('product_kit_id')
- quantity = item_data.get('quantity', 1)
- price_raw = item_data.get('price', '')
-
- # Конвертируем количество в Decimal
- try:
- quantity = Decimal(str(quantity))
- except (ValueError, TypeError, decimal.InvalidOperation):
- continue
-
- # Получаем товар или комплект
- product = None
- product_kit = None
-
- if product_id:
- try:
- product = Product.objects.get(pk=product_id)
- except Product.DoesNotExist:
- continue
- elif product_kit_id:
- try:
- product_kit = ProductKit.objects.get(pk=product_kit_id)
- except ProductKit.DoesNotExist:
- continue
- else:
- continue
-
- # Определяем оригинальную цену из каталога
- original_price = product.actual_price if product else product_kit.actual_price
-
- # Конвертируем цену в Decimal, если пустая - используем оригинальную
- try:
- price = Decimal(str(price_raw)) if price_raw else Decimal('0')
- # Если цена 0 или пустая, используем оригинальную цену
- if price == Decimal('0'):
- price = original_price
- is_custom_price = False
- else:
- # Определяем, изменилась ли цена
- is_custom_price = abs(price - original_price) > Decimal('0.01')
- except (ValueError, TypeError, decimal.InvalidOperation):
- # В случае ошибки используем оригинальную цену
- price = original_price
- is_custom_price = False
-
- # Обновляем существующую позицию или создаём новую
- if item_id:
- # Обновляем существующую позицию
- try:
- item = OrderItem.objects.get(id=item_id, order=order)
- item.product = product
- item.product_kit = product_kit
- item.quantity = quantity
- item.price = price
- item.is_custom_price = is_custom_price
- item.save()
- except OrderItem.DoesNotExist:
- # Если позиция не найдена, создаём новую
- OrderItem.objects.create(
- order=order,
- product=product,
- product_kit=product_kit,
- quantity=quantity,
- price=price,
- is_custom_price=is_custom_price
- )
- else:
- # Создаём новую позицию
- OrderItem.objects.create(
- order=order,
- product=product,
- product_kit=product_kit,
- quantity=quantity,
- price=price,
- is_custom_price=is_custom_price
- )
-
- # Обрабатываем удаление платежей
- if 'deleted_payment_ids' in data:
- deleted_payment_ids = data['deleted_payment_ids']
- if deleted_payment_ids:
- from ..models import Payment
- Payment.objects.filter(id__in=deleted_payment_ids, order=order).delete()
-
- # Обрабатываем платежи (payments)
- if 'payments' in data:
- from ..models import Payment, PaymentMethod
- payments_data = data['payments']
-
- # Обрабатываем каждый платеж
- for payment_data in payments_data:
- payment_id = payment_data.get('id') # ID существующего платежа (если есть)
- payment_method_id = payment_data.get('payment_method_id')
- amount_raw = payment_data.get('amount', '')
- notes = payment_data.get('notes', '')
-
- # Пропускаем пустые платежи
- if not payment_method_id or not amount_raw:
- continue
-
- # Конвертируем сумму в Decimal
- try:
- amount = Decimal(str(amount_raw))
- if amount <= 0:
- continue
- except (ValueError, TypeError, decimal.InvalidOperation):
- continue
-
- # Получаем способ оплаты
- try:
- payment_method = PaymentMethod.objects.get(pk=payment_method_id)
- except PaymentMethod.DoesNotExist:
- continue
-
- # Обновляем существующий платеж или создаём новый
- if payment_id:
- # Обновляем существующий платеж
- try:
- payment = Payment.objects.get(id=payment_id, order=order)
- payment.payment_method = payment_method
- payment.amount = amount
- payment.notes = notes
- payment.save()
- except Payment.DoesNotExist:
- # Если платеж не найден, создаём новый
- Payment.objects.create(
- order=order,
- payment_method=payment_method,
- amount=amount,
- notes=notes,
- created_by=user
- )
- else:
- # Создаём новый платеж
- Payment.objects.create(
- order=order,
- payment_method=payment_method,
- amount=amount,
- notes=notes,
- created_by=user
- )
-
- 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_warehouse')
-
- 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
diff --git a/myproject/orders/services/order_status_service.py b/myproject/orders/services/order_status_service.py
index 8ddcd87..827ba11 100644
--- a/myproject/orders/services/order_status_service.py
+++ b/myproject/orders/services/order_status_service.py
@@ -27,6 +27,21 @@ class OrderStatusService:
except OrderStatus.DoesNotExist:
return None
+ @staticmethod
+ def get_new_status():
+ """Возвращает системный статус 'new' (новый заказ)"""
+ status, created = OrderStatus.objects.get_or_create(
+ code='new',
+ defaults={
+ 'name': 'Новый',
+ 'label': 'Новый',
+ 'is_system': True,
+ 'color': '#0d6efd',
+ 'order': 10,
+ }
+ )
+ return status
+
@staticmethod
def get_system_status(code):
"""Получить системный статус по коду"""
diff --git a/myproject/orders/static/orders/js/autosave.js b/myproject/orders/static/orders/js/autosave.js
deleted file mode 100644
index 44baf61..0000000
--- a/myproject/orders/static/orders/js/autosave.js
+++ /dev/null
@@ -1,664 +0,0 @@
-/**
- * Модуль автосохранения черновиков заказов.
- *
- * Автоматически сохраняет изменения в черновике заказа при изменении полей формы.
- * Использует debouncing для уменьшения количества запросов к серверу.
- */
-
-(function() {
- 'use strict';
-
- // Конфигурация
- const CONFIG = {
- AUTOSAVE_DELAY: 3000, // Задержка перед автосохранением (мс)
- AUTOSAVE_URL_PATTERN: '/orders/{orderNumber}/autosave/',
- STATUS_DISPLAY_DURATION: 5000, // Длительность показа статуса (мс)
- };
-
- // Состояние модуля
- let autosaveTimer = null;
- let isAutosaving = false;
- let orderNumber = null;
-
- /**
- * Инициализация модуля автосохранения
- */
- function init() {
- // Проверяем, что мы на странице редактирования
- const isEditPage = window.location.pathname.includes('/edit/');
- if (!isEditPage) {
- return;
- }
-
- const orderForm = document.getElementById('order-form');
- if (!orderForm) {
- return;
- }
-
- // Получаем номер заказа из URL
- const urlMatch = window.location.pathname.match(/\/orders\/(\d+)\/edit\//);
- if (!urlMatch) {
- return;
- }
- orderNumber = urlMatch[1];
-
- // Инициализируем UI индикатора
- initStatusIndicator();
-
- // Добавляем обработчики событий
- attachEventListeners();
- }
-
- /**
- * Создает индикатор статуса автосохранения
- */
- function initStatusIndicator() {
- // Проверяем, не создан ли уже индикатор
- if (document.getElementById('autosave-status')) {
- return;
- }
-
- const indicator = document.createElement('div');
- indicator.id = 'autosave-status';
- indicator.className = 'alert alert-info';
- indicator.style.cssText = `
- position: fixed;
- top: 70px;
- right: 20px;
- z-index: 1050;
- min-width: 250px;
- display: none;
- padding: 10px 15px;
- border-radius: 4px;
- box-shadow: 0 2px 8px rgba(0,0,0,0.15);
- `;
- indicator.innerHTML = `
-
-
- Автосохранение...
-
- `;
-
- document.body.appendChild(indicator);
- }
-
- /**
- * Показывает статус автосохранения
- */
- function showStatus(type, message) {
- const indicator = document.getElementById('autosave-status');
- const icon = document.getElementById('autosave-icon');
- const text = document.getElementById('autosave-text');
-
- if (!indicator || !icon || !text) {
- return;
- }
-
- // Убираем все классы
- indicator.className = 'alert';
-
- // Устанавливаем соответствующий класс и иконку
- switch (type) {
- case 'saving':
- indicator.classList.add('alert-info');
- icon.innerHTML = '';
- break;
- case 'success':
- indicator.classList.add('alert-success');
- icon.innerHTML = '';
- break;
- case 'error':
- indicator.classList.add('alert-danger');
- icon.innerHTML = '';
- break;
- }
-
- text.textContent = message;
- indicator.style.display = 'block';
-
- // Автоматически скрываем статус (кроме ошибок)
- if (type !== 'error') {
- setTimeout(() => {
- indicator.style.display = 'none';
- }, CONFIG.STATUS_DISPLAY_DURATION);
- }
- }
-
- /**
- * Прикрепляет обработчики событий к полям формы
- */
- function attachEventListeners() {
- const form = document.getElementById('order-form');
- if (!form) {
- return;
- }
-
- // Слушаем изменения в основных полях заказа
- const fieldsToWatch = [
- 'select[name="customer"]',
- 'select[name="status"]',
- 'input[name="delivery_date"]',
- 'input[name="delivery_time_start"]',
- 'input[name="delivery_time_end"]',
- 'input[name="delivery_cost"]',
- 'textarea[name="special_instructions"]',
- 'input[name="discount_amount"]',
- 'input[type="checkbox"]',
- 'input[type="radio"]',
- 'select[name="delivery_address"]',
- 'select[name="pickup_warehouse"]',
- // Поля адреса доставки
- 'input[name="address_street"]',
- 'input[name="address_building_number"]',
- 'input[name="address_apartment_number"]',
- 'input[name="address_entrance"]',
- 'input[name="address_floor"]',
- 'input[name="address_intercom_code"]',
- 'textarea[name="address_delivery_instructions"]',
- // Поля получателя
- 'input[name="recipient_name"]',
- 'input[name="recipient_phone"]',
- ];
-
- fieldsToWatch.forEach(selector => {
- const fields = form.querySelectorAll(selector);
- fields.forEach(field => {
- // Для select и checkbox используем 'change'
- if (field.tagName === 'SELECT' || field.type === 'checkbox' || field.type === 'radio') {
- field.addEventListener('change', scheduleAutosave);
- } else {
- // Для текстовых полей используем 'input'
- field.addEventListener('input', scheduleAutosave);
- }
- });
- });
-
- // Слушаем изменения в формах товаров (formset)
- observeFormsetChanges();
-
- // Слушаем изменения в формах платежей (payment formset)
- observePaymentFormsetChanges();
- }
-
- /**
- * Наблюдает за изменениями в формсете товаров
- */
- function observeFormsetChanges() {
- const formsetContainer = document.getElementById('order-items-container');
- if (!formsetContainer) {
- return;
- }
-
- // Наблюдаем за добавлением/удалением форм
- const observer = new MutationObserver(() => {
- attachFormsetEventListeners();
- });
-
- observer.observe(formsetContainer, {
- childList: true,
- subtree: true
- });
-
- // Прикрепляем обработчики к существующим формам
- attachFormsetEventListeners();
- }
-
- /**
- * Прикрепляет обработчики к полям в формах товаров
- */
- function attachFormsetEventListeners() {
- const itemForms = document.querySelectorAll('.order-item-form');
-
- itemForms.forEach(form => {
- // Если уже прикреплены обработчики, пропускаем
- if (form.dataset.autosaveAttached === 'true') {
- return;
- }
-
- const fields = form.querySelectorAll('select, input[type="number"], input[type="text"], input[type="checkbox"]');
-
- fields.forEach(field => {
- if (field.tagName === 'SELECT' || field.type === 'checkbox') {
- field.addEventListener('change', scheduleAutosave);
-
- // Для Select2 добавляем дополнительный обработчик
- if (window.jQuery && jQuery(field).data('select2')) {
- jQuery(field).on('select2:select', scheduleAutosave);
- }
- } else {
- field.addEventListener('input', scheduleAutosave);
- }
- });
-
- form.dataset.autosaveAttached = 'true';
- });
- }
-
- /**
- * Наблюдает за изменениями в формсете платежей
- */
- function observePaymentFormsetChanges() {
- const paymentsContainer = document.getElementById('payments-container');
- if (!paymentsContainer) {
- return;
- }
-
- // Наблюдаем за добавлением/удалением форм платежей
- const observer = new MutationObserver(() => {
- attachPaymentFormsetEventListeners();
- });
-
- observer.observe(paymentsContainer, {
- childList: true,
- subtree: true
- });
-
- // Прикрепляем обработчики к существующим формам
- attachPaymentFormsetEventListeners();
- }
-
- /**
- * Прикрепляет обработчики к полям в формах платежей
- */
- function attachPaymentFormsetEventListeners() {
- const paymentForms = document.querySelectorAll('.payment-form');
-
- paymentForms.forEach(form => {
- // Если уже прикреплены обработчики, пропускаем
- if (form.dataset.autosavePaymentAttached === 'true') {
- return;
- }
-
- const fields = form.querySelectorAll('select, input[type="number"], input[type="text"], textarea, input[type="checkbox"]');
-
- fields.forEach(field => {
- if (field.tagName === 'SELECT' || field.type === 'checkbox') {
- field.addEventListener('change', scheduleAutosave);
- } else {
- field.addEventListener('input', scheduleAutosave);
- }
- });
-
- form.dataset.autosavePaymentAttached = 'true';
- });
- }
-
- /**
- * Планирует автосохранение с задержкой (debouncing)
- */
- function scheduleAutosave() {
- // Отменяем предыдущий таймер
- if (autosaveTimer) {
- clearTimeout(autosaveTimer);
- }
-
- // Устанавливаем новый таймер
- autosaveTimer = setTimeout(() => {
- performAutosave();
- }, CONFIG.AUTOSAVE_DELAY);
- }
-
- /**
- * Выполняет автосохранение
- */
- async function performAutosave() {
- if (isAutosaving) {
- return;
- }
-
- isAutosaving = true;
- showStatus('saving', 'Сохранение...');
-
- try {
- // Собираем данные формы
- const formData = collectFormData();
-
- // Отправляем AJAX запрос
- const url = CONFIG.AUTOSAVE_URL_PATTERN.replace('{orderNumber}', orderNumber);
- const response = await fetch(url, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-CSRFToken': getCsrfToken(),
- },
- body: JSON.stringify(formData)
- });
-
- const data = await response.json();
-
- if (response.ok && data.success) {
- const lastSaved = formatDateTime(data.last_saved);
- showStatus('success', 'Сохранено ' + lastSaved);
- } else {
- showStatus('error', 'Ошибка: ' + (data.error || 'Неизвестная ошибка'));
- }
-
- } catch (error) {
- showStatus('error', 'Ошибка соединения с сервером');
- } finally {
- isAutosaving = false;
- }
- }
-
- /**
- * Собирает данные формы для отправки
- */
- function collectFormData() {
- const form = document.getElementById('order-form');
- const data = {};
-
- // Основные поля заказа
- const customerField = form.querySelector('select[name="customer"]');
- if (customerField && customerField.value) {
- data.customer = parseInt(customerField.value);
- }
-
- const statusField = form.querySelector('select[name="status"]');
- if (statusField && statusField.value) {
- data.status = parseInt(statusField.value);
- }
-
- const deliveryDateField = form.querySelector('input[name="delivery_date"]');
- if (deliveryDateField && deliveryDateField.value) {
- data.delivery_date = deliveryDateField.value;
- }
-
- const deliveryTimeStartField = form.querySelector('input[name="delivery_time_start"]');
- if (deliveryTimeStartField && deliveryTimeStartField.value) {
- data.delivery_time_start = deliveryTimeStartField.value;
- }
-
- const deliveryTimeEndField = form.querySelector('input[name="delivery_time_end"]');
- if (deliveryTimeEndField && deliveryTimeEndField.value) {
- data.delivery_time_end = deliveryTimeEndField.value;
- }
-
- const deliveryCostField = form.querySelector('input[name="delivery_cost"]');
- if (deliveryCostField && deliveryCostField.value) {
- data.delivery_cost = deliveryCostField.value;
- }
-
- const specialInstructionsField = form.querySelector('textarea[name="special_instructions"]');
- if (specialInstructionsField) {
- data.special_instructions = specialInstructionsField.value;
- }
-
- const discountAmountField = form.querySelector('input[name="discount_amount"]');
- if (discountAmountField && discountAmountField.value) {
- data.discount_amount = discountAmountField.value;
- }
-
- // Checkbox поля
- const isDeliveryField = form.querySelector('input[name="is_delivery"]');
- if (isDeliveryField) {
- data.is_delivery = isDeliveryField.checked;
- }
-
- const customerIsRecipientField = form.querySelector('input[name="customer_is_recipient"]');
- if (customerIsRecipientField) {
- data.customer_is_recipient = customerIsRecipientField.checked;
- }
-
- const isAnonymousField = form.querySelector('input[name="is_anonymous"]');
- if (isAnonymousField) {
- data.is_anonymous = isAnonymousField.checked;
- }
-
- // Адрес доставки или точка самовывоза
- const deliveryAddressField = form.querySelector('select[name="delivery_address"]');
- if (deliveryAddressField && deliveryAddressField.value) {
- data.delivery_address = parseInt(deliveryAddressField.value);
- }
-
- const pickupWarehouseField = form.querySelector('select[name="pickup_warehouse"]');
- if (pickupWarehouseField && pickupWarehouseField.value) {
- data.pickup_warehouse = parseInt(pickupWarehouseField.value);
- }
-
- // Поля адреса доставки (новая логика с прямым вводом)
- const addressStreetField = form.querySelector('input[name="address_street"]');
- const addressBuildingField = form.querySelector('input[name="address_building_number"]');
- const addressApartmentField = form.querySelector('input[name="address_apartment_number"]');
- const addressEntranceField = form.querySelector('input[name="address_entrance"]');
- const addressFloorField = form.querySelector('input[name="address_floor"]');
- const addressIntercomField = form.querySelector('input[name="address_intercom_code"]');
- const addressInstructionsField = form.querySelector('textarea[name="address_delivery_instructions"]');
-
- // Собираем все поля адреса
- const addressFields = {
- address_street: addressStreetField?.value || '',
- address_building_number: addressBuildingField?.value || '',
- address_apartment_number: addressApartmentField?.value || '',
- address_entrance: addressEntranceField?.value || '',
- address_floor: addressFloorField?.value || '',
- address_intercom_code: addressIntercomField?.value || '',
- address_delivery_instructions: addressInstructionsField?.value || '',
- };
-
- // Проверяем, заполнено ли хотя бы одно поле адреса
- const hasAnyAddressData = Object.values(addressFields).some(value => value.trim() !== '');
-
- if (hasAnyAddressData) {
- // Указываем режим "новый адрес" если заполнено хотя бы одно поле
- data.address_mode = 'new';
-
- // Добавляем все непустые поля в данные
- Object.entries(addressFields).forEach(([key, value]) => {
- if (value.trim() !== '') {
- data[key] = value;
- }
- });
- }
-
- const addressConfirmField = form.querySelector('input[name="address_confirm_with_recipient"]');
- if (addressConfirmField) {
- data.address_confirm_with_recipient = addressConfirmField.checked;
- }
-
- // Поля получателя
- const recipientNameField = form.querySelector('input[name="recipient_name"]');
- if (recipientNameField && recipientNameField.value) {
- data.recipient_name = recipientNameField.value;
- }
-
- const recipientPhoneField = form.querySelector('input[name="recipient_phone"]');
- if (recipientPhoneField && recipientPhoneField.value) {
- data.recipient_phone = recipientPhoneField.value;
- }
-
- // Собираем позиции заказа
- const orderItemsData = collectOrderItems();
- data.items = orderItemsData.items;
- data.deleted_item_ids = orderItemsData.deletedItemIds;
-
- // Собираем платежи
- const paymentsData = collectPayments();
- data.payments = paymentsData.payments;
- data.deleted_payment_ids = paymentsData.deletedPaymentIds;
-
- // Флаг для пересчета итоговой суммы
- data.recalculate = true;
-
- return data;
- }
-
- /**
- * Собирает данные о позициях заказа
- */
- function collectOrderItems() {
- const items = [];
- const deletedItemIds = [];
- const itemForms = document.querySelectorAll('.order-item-form');
-
- itemForms.forEach(form => {
- // Проверяем, помечена ли форма на удаление
- const deleteCheckbox = form.querySelector('input[name$="-DELETE"]');
- const idField = form.querySelector('input[name$="-id"]');
-
- if (deleteCheckbox && deleteCheckbox.checked) {
- // Если форма помечена на удаление и имеет ID, добавляем в список удалённых
- if (idField && idField.value) {
- deletedItemIds.push(parseInt(idField.value));
- }
- return; // Не добавляем в items
- }
-
- // Получаем выбранный товар/комплект
- const itemSelect = form.querySelector('.select2-order-item');
- if (!itemSelect || !itemSelect.value) {
- return;
- }
-
- const itemValue = itemSelect.value;
- const quantityInput = form.querySelector('input[name$="-quantity"]');
- const priceInput = form.querySelector('input[name$="-price"]');
-
- if (!quantityInput || !priceInput) {
- return;
- }
-
- const item = {
- quantity: quantityInput.value || '1',
- price: (priceInput.value || '0').replace(',', '.')
- };
-
- // Если есть ID (существующий товар), добавляем его
- if (idField && idField.value) {
- item.id = parseInt(idField.value);
- }
-
- // Определяем тип: товар или комплект
- if (itemValue.startsWith('product_')) {
- item.product_id = parseInt(itemValue.replace('product_', ''));
- } else if (itemValue.startsWith('kit_')) {
- item.product_kit_id = parseInt(itemValue.replace('kit_', ''));
- }
-
- items.push(item);
- });
-
- return { items, deletedItemIds };
- }
-
- /**
- * Собирает данные о платежах
- */
- function collectPayments() {
- const payments = [];
- const deletedPaymentIds = [];
- const paymentForms = document.querySelectorAll('.payment-form');
-
- paymentForms.forEach(form => {
- // Проверяем, помечена ли форма на удаление
- const deleteCheckbox = form.querySelector('input[name$="-DELETE"]');
- const idField = form.querySelector('input[name$="-id"]');
-
- if (deleteCheckbox && deleteCheckbox.checked) {
- // Если форма помечена на удаление и имеет ID, добавляем в список удалённых
- if (idField && idField.value) {
- deletedPaymentIds.push(parseInt(idField.value));
- }
- return; // Не добавляем в payments
- }
-
- // Получаем способ оплаты и сумму
- const paymentMethodSelect = form.querySelector('select[name$="-payment_method"]');
- const amountInput = form.querySelector('input[name$="-amount"]');
- const notesInput = form.querySelector('textarea[name$="-notes"]');
-
- if (!paymentMethodSelect || !paymentMethodSelect.value || !amountInput || !amountInput.value) {
- return; // Пропускаем пустые платежи
- }
-
- const payment = {
- payment_method_id: parseInt(paymentMethodSelect.value),
- amount: (amountInput.value || '0').replace(',', '.'),
- notes: notesInput ? notesInput.value : ''
- };
-
- // Если есть ID (существующий платеж), добавляем его
- if (idField && idField.value) {
- payment.id = parseInt(idField.value);
- }
-
- payments.push(payment);
- });
-
- return { payments, deletedPaymentIds };
- }
-
- /**
- * Получает CSRF токен из cookies или meta тега
- */
- function getCsrfToken() {
- // Пробуем получить из cookie
- const name = 'csrftoken';
- let cookieValue = null;
- if (document.cookie && document.cookie !== '') {
- const cookies = document.cookie.split(';');
- for (let i = 0; i < cookies.length; i++) {
- const cookie = cookies[i].trim();
- if (cookie.substring(0, name.length + 1) === (name + '=')) {
- cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
- break;
- }
- }
- }
-
- // Если не нашли в cookie, пробуем в meta теге
- if (!cookieValue) {
- const metaTag = document.querySelector('meta[name="csrf-token"]');
- if (metaTag) {
- cookieValue = metaTag.getAttribute('content');
- }
- }
-
- // Если не нашли в meta теге, пробуем в input поле
- if (!cookieValue) {
- const csrfInput = document.querySelector('input[name="csrfmiddlewaretoken"]');
- if (csrfInput) {
- cookieValue = csrfInput.value;
- }
- }
-
- return cookieValue;
- }
-
- /**
- * Форматирует дату и время для отображения
- */
- function formatDateTime(isoString) {
- if (!isoString) {
- return 'только что';
- }
-
- const date = new Date(isoString);
- const now = new Date();
- const diffMs = now - date;
- const diffSecs = Math.floor(diffMs / 1000);
- const diffMins = Math.floor(diffSecs / 60);
-
- if (diffSecs < 60) {
- return 'только что';
- } else if (diffMins < 60) {
- return diffMins + ' мин. назад';
- } else {
- const hours = date.getHours().toString().padStart(2, '0');
- const minutes = date.getMinutes().toString().padStart(2, '0');
- return 'в ' + hours + ':' + minutes;
- }
- }
-
- // Инициализация при загрузке DOM
- if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', init);
- } else {
- init();
- }
-
- // Экспортируем функцию scheduleAutosave в глобальную область
- window.orderAutosave = {
- scheduleAutosave: scheduleAutosave
- };
-
-})();
diff --git a/myproject/orders/static/orders/js/draft-creator.js b/myproject/orders/static/orders/js/draft-creator.js
deleted file mode 100644
index 7e125fb..0000000
--- a/myproject/orders/static/orders/js/draft-creator.js
+++ /dev/null
@@ -1,440 +0,0 @@
-/**
- * Модуль автоматического создания черновика заказа.
- *
- * При первом изменении формы создания заказа автоматически создаёт черновик
- * и перенаправляет пользователя на страницу редактирования черновика,
- * где уже работает обычное автосохранение.
- */
-
-(function() {
- 'use strict';
-
- // Конфигурация
- const CONFIG = {
- CREATE_DRAFT_URL: '/orders/create-draft/',
- DEBOUNCE_DELAY: 2000, // Задержка перед созданием черновика (мс)
- };
-
- // Состояние модуля
- let createDraftTimer = null;
- let isCreatingDraft = false;
- let draftCreated = false;
-
- /**
- * Инициализация модуля
- */
- function init() {
- // Проверяем, что мы на странице создания заказа
- const isCreatePage = window.location.pathname.includes('/orders/create/');
- if (!isCreatePage) {
- console.log('[DraftCreator] Not on create page, exiting');
- return;
- }
-
- const orderForm = document.getElementById('order-form');
- if (!orderForm) {
- console.log('[DraftCreator] Order form not found, exiting');
- return;
- }
-
- // Проверяем, что это не черновик (для черновиков есть autosave.js)
- if (orderForm.dataset.isDraft === 'true') {
- console.log('[DraftCreator] This is a draft, exiting (autosave.js will handle it)');
- return;
- }
-
- console.log('[DraftCreator] Initialized on order create page');
-
- // Добавляем обработчики событий
- attachEventListeners();
- }
-
- /**
- * Прикрепляет обработчики событий к полям формы
- */
- function attachEventListeners() {
- const form = document.getElementById('order-form');
- if (!form) {
- return;
- }
-
- // Слушаем изменения в поле клиента (обязательное поле)
- const customerField = form.querySelector('select[name="customer"]');
- if (customerField) {
- // Обычное событие change
- customerField.addEventListener('change', function() {
- console.log('[DraftCreator] Customer changed (native event):', this.value);
- if (this.value && !draftCreated) {
- scheduleCreateDraft();
- }
- });
-
- // Событие Select2
- if (window.jQuery && jQuery(customerField).data('select2')) {
- jQuery(customerField).on('select2:select', function(e) {
- console.log('[DraftCreator] Customer changed (select2 event):', e.params.data.id);
- if (e.params.data.id && !draftCreated) {
- scheduleCreateDraft();
- }
- });
- }
- }
-
- // Черновик создаётся ТОЛЬКО при выборе клиента.
- // После создания и переадресации на страницу редактирования
- // уже работает полноценное автосохранение для всех полей и товаров.
- }
-
- /**
- * Планирует создание черновика с задержкой (debouncing)
- */
- function scheduleCreateDraft() {
- // Отменяем предыдущий таймер
- if (createDraftTimer) {
- clearTimeout(createDraftTimer);
- }
-
- // Устанавливаем новый таймер
- createDraftTimer = setTimeout(() => {
- createDraft();
- }, CONFIG.DEBOUNCE_DELAY);
-
- console.log('[DraftCreator] Scheduled draft creation in ' + CONFIG.DEBOUNCE_DELAY + 'ms');
- }
-
- /**
- * Создаёт черновик заказа
- */
- async function createDraft() {
- if (isCreatingDraft || draftCreated) {
- console.log('[DraftCreator] Already creating or created, skipping');
- return;
- }
-
- isCreatingDraft = true;
- console.log('[DraftCreator] Creating draft...');
-
- try {
- // Собираем данные формы
- const formData = collectFormData();
-
- // Проверяем наличие клиента
- if (!formData.customer) {
- console.log('[DraftCreator] No customer selected, skipping');
- isCreatingDraft = false;
- return;
- }
-
- // Отправляем AJAX запрос
- const response = await fetch(CONFIG.CREATE_DRAFT_URL, {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-CSRFToken': getCsrfToken(),
- },
- body: JSON.stringify(formData)
- });
-
- const data = await response.json();
-
- if (response.ok && data.success) {
- console.log('[DraftCreator] Draft created successfully:', data);
- draftCreated = true;
-
- // Показываем уведомление
- showNotification('Черновик создан. Перенаправление...');
-
- // Перенаправляем на страницу редактирования черновика
- setTimeout(() => {
- window.location.href = data.redirect_url;
- }, 500);
-
- } else {
- console.error('[DraftCreator] Error creating draft:', data);
-
- // Если ошибка не критичная (например, клиент не выбран), не показываем
- if (response.status !== 400) {
- showNotification('Ошибка создания черновика: ' + (data.error || 'Неизвестная ошибка'), 'error');
- }
- }
-
- } catch (error) {
- console.error('[DraftCreator] Exception:', error);
- showNotification('Ошибка соединения с сервером', 'error');
- } finally {
- isCreatingDraft = false;
- }
- }
-
- /**
- * Собирает данные формы для отправки
- */
- function collectFormData() {
- const form = document.getElementById('order-form');
- const data = {};
-
- // Основные поля заказа
- const customerField = form.querySelector('select[name="customer"]');
- if (customerField && customerField.value) {
- data.customer = parseInt(customerField.value);
- }
-
- const deliveryDateField = form.querySelector('input[name="delivery_date"]');
- if (deliveryDateField && deliveryDateField.value) {
- data.delivery_date = deliveryDateField.value;
- }
-
- const deliveryTimeStartField = form.querySelector('input[name="delivery_time_start"]');
- if (deliveryTimeStartField && deliveryTimeStartField.value) {
- data.delivery_time_start = deliveryTimeStartField.value;
- }
-
- const deliveryTimeEndField = form.querySelector('input[name="delivery_time_end"]');
- if (deliveryTimeEndField && deliveryTimeEndField.value) {
- data.delivery_time_end = deliveryTimeEndField.value;
- }
-
- const deliveryCostField = form.querySelector('input[name="delivery_cost"]');
- if (deliveryCostField && deliveryCostField.value) {
- data.delivery_cost = deliveryCostField.value;
- }
-
- const paymentMethodField = form.querySelector('select[name="payment_method"]');
- if (paymentMethodField && paymentMethodField.value) {
- data.payment_method = paymentMethodField.value;
- }
-
- const specialInstructionsField = form.querySelector('textarea[name="special_instructions"]');
- if (specialInstructionsField) {
- data.special_instructions = specialInstructionsField.value;
- }
-
- const discountAmountField = form.querySelector('input[name="discount_amount"]');
- if (discountAmountField && discountAmountField.value) {
- data.discount_amount = discountAmountField.value;
- }
-
- // Checkbox поля
- const isDeliveryField = form.querySelector('input[name="is_delivery"]');
- if (isDeliveryField) {
- data.is_delivery = isDeliveryField.checked;
- }
-
- const customerIsRecipientField = form.querySelector('input[name="customer_is_recipient"]');
- if (customerIsRecipientField) {
- data.customer_is_recipient = customerIsRecipientField.checked;
- }
-
- const isAnonymousField = form.querySelector('input[name="is_anonymous"]');
- if (isAnonymousField) {
- data.is_anonymous = isAnonymousField.checked;
- }
-
- // Адрес доставки или точка самовывоза
- const deliveryAddressField = form.querySelector('select[name="delivery_address"]');
- if (deliveryAddressField && deliveryAddressField.value) {
- data.delivery_address = parseInt(deliveryAddressField.value);
- }
-
- const pickupWarehouseField = form.querySelector('select[name="pickup_warehouse"]');
- if (pickupWarehouseField && pickupWarehouseField.value) {
- data.pickup_warehouse = parseInt(pickupWarehouseField.value);
- }
-
- // Новая логика выбора адреса
- const addressModeField = form.querySelector('input[name="address_mode"]:checked');
- if (addressModeField) {
- data.address_mode = addressModeField.value;
-
- if (addressModeField.value === 'history') {
- const addressFromHistoryField = form.querySelector('select[name="address_from_history"]');
- if (addressFromHistoryField && addressFromHistoryField.value) {
- data.address_from_history = parseInt(addressFromHistoryField.value);
- }
- } else if (addressModeField.value === 'new') {
- // Собираем поля нового адреса
- const addressStreetField = form.querySelector('input[name="address_street"]');
- if (addressStreetField && addressStreetField.value) {
- data.address_street = addressStreetField.value;
- }
-
- const addressBuildingField = form.querySelector('input[name="address_building_number"]');
- if (addressBuildingField && addressBuildingField.value) {
- data.address_building_number = addressBuildingField.value;
- }
-
- const addressApartmentField = form.querySelector('input[name="address_apartment_number"]');
- if (addressApartmentField && addressApartmentField.value) {
- data.address_apartment_number = addressApartmentField.value;
- }
-
- const addressEntranceField = form.querySelector('input[name="address_entrance"]');
- if (addressEntranceField && addressEntranceField.value) {
- data.address_entrance = addressEntranceField.value;
- }
-
- const addressFloorField = form.querySelector('input[name="address_floor"]');
- if (addressFloorField && addressFloorField.value) {
- data.address_floor = addressFloorField.value;
- }
-
- const addressIntercomField = form.querySelector('input[name="address_intercom_code"]');
- if (addressIntercomField && addressIntercomField.value) {
- data.address_intercom_code = addressIntercomField.value;
- }
-
- const addressInstructionsField = form.querySelector('textarea[name="address_delivery_instructions"]');
- if (addressInstructionsField && addressInstructionsField.value) {
- data.address_delivery_instructions = addressInstructionsField.value;
- }
-
- const addressConfirmField = form.querySelector('input[name="address_confirm_with_recipient"]');
- if (addressConfirmField) {
- data.address_confirm_with_recipient = addressConfirmField.checked;
- }
- }
- }
-
- // Поля получателя
- const recipientNameField = form.querySelector('input[name="recipient_name"]');
- if (recipientNameField && recipientNameField.value) {
- data.recipient_name = recipientNameField.value;
- }
-
- const recipientPhoneField = form.querySelector('input[name="recipient_phone"]');
- if (recipientPhoneField && recipientPhoneField.value) {
- data.recipient_phone = recipientPhoneField.value;
- }
-
- // Собираем позиции заказа
- const items = collectOrderItems();
- if (items.length > 0) {
- data.items = items;
- }
-
- return data;
- }
-
- /**
- * Собирает данные о позициях заказа
- */
- function collectOrderItems() {
- const items = [];
- const itemForms = document.querySelectorAll('.order-item-form');
-
- itemForms.forEach((form, index) => {
- // Пропускаем удаленные формы
- const deleteCheckbox = form.querySelector('input[name$="-DELETE"]');
- if (deleteCheckbox && deleteCheckbox.checked) {
- return;
- }
-
- // Получаем выбранный товар/комплект
- const itemSelect = form.querySelector('.select2-order-item');
- if (!itemSelect || !itemSelect.value) {
- return;
- }
-
- const itemValue = itemSelect.value;
- const quantityInput = form.querySelector('input[name$="-quantity"]');
- const priceInput = form.querySelector('input[name$="-price"]');
-
- if (!quantityInput || !priceInput) {
- return;
- }
-
- const item = {
- quantity: quantityInput.value || '1',
- price: priceInput.value || '0'
- };
-
- // Определяем тип: товар или комплект
- if (itemValue.startsWith('product_')) {
- item.product_id = parseInt(itemValue.replace('product_', ''));
- } else if (itemValue.startsWith('kit_')) {
- item.product_kit_id = parseInt(itemValue.replace('kit_', ''));
- }
-
- items.push(item);
- });
-
- return items;
- }
-
- /**
- * Получает CSRF токен
- */
- function getCsrfToken() {
- // Пробуем получить из cookie
- const name = 'csrftoken';
- let cookieValue = null;
- if (document.cookie && document.cookie !== '') {
- const cookies = document.cookie.split(';');
- for (let i = 0; i < cookies.length; i++) {
- const cookie = cookies[i].trim();
- if (cookie.substring(0, name.length + 1) === (name + '=')) {
- cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
- break;
- }
- }
- }
-
- // Если не нашли в cookie, пробуем в input поле
- if (!cookieValue) {
- const csrfInput = document.querySelector('input[name="csrfmiddlewaretoken"]');
- if (csrfInput) {
- cookieValue = csrfInput.value;
- }
- }
-
- return cookieValue;
- }
-
- /**
- * Показывает уведомление пользователю
- */
- function showNotification(message, type = 'info') {
- // Создаём простое уведомление
- const notification = document.createElement('div');
- notification.className = `alert alert-${type === 'error' ? 'danger' : 'info'}`;
- notification.style.cssText = `
- position: fixed;
- top: 70px;
- right: 20px;
- z-index: 1050;
- min-width: 250px;
- padding: 10px 15px;
- border-radius: 4px;
- box-shadow: 0 2px 8px rgba(0,0,0,0.15);
- `;
- notification.innerHTML = `
-
- ${type === 'error' ? '⚠️' : 'ℹ️'}
- ${message}
-
- `;
-
- document.body.appendChild(notification);
-
- // Автоматически удаляем через 3 секунды
- setTimeout(() => {
- notification.remove();
- }, 3000);
- }
-
- // Инициализация при загрузке DOM
- if (document.readyState === 'loading') {
- document.addEventListener('DOMContentLoaded', init);
- } else {
- init();
- }
-
- // Экспортируем публичный API для вызова из модального окна
- window.DraftCreator = {
- triggerDraftCreation: function() {
- console.log('[DraftCreator] Triggered via API');
- scheduleCreateDraft();
- }
- };
-
-})();
diff --git a/myproject/orders/templates/orders/order_form.html b/myproject/orders/templates/orders/order_form.html
index b6924a3..29f13c6 100644
--- a/myproject/orders/templates/orders/order_form.html
+++ b/myproject/orders/templates/orders/order_form.html
@@ -831,12 +831,34 @@
-
-
- Отмена
-
+
+ {% if is_create_page %}
+
+
+
+ {% elif is_draft %}
+
+
+
+ {% else %}
+
+
+ {% endif %}
+
+
+ Отмена
+
+
@@ -1000,7 +1022,7 @@ function initCustomerSelect2() {
console.log('Значение восстановлено:', $customerSelect.val());
}
- // Уведомляем draft-creator.js что Select2 готов и есть предзаполненное значение
+ // Select2 готов и есть предзаполненное значение
if (currentValue && window.DraftCreator) {
console.log('7. Уведомляем DraftCreator о предзаполненном клиенте');
setTimeout(function() {
@@ -1065,12 +1087,12 @@ function initCustomerSelect2() {
if (data.is_create_option || data.id === '__create_new__') {
console.log('11. Открываем модальное окно для создания клиента');
this.value = '';
- // Триггерим нативное change событие для draft-creator.js
+ // Триггерим нативное change событие
const changeEvent = new Event('change', { bubbles: true });
this.dispatchEvent(changeEvent);
window.openCreateCustomerModal(data.search_text);
} else {
- // Триггерим нативное change событие для других обработчиков (например, draft-creator.js)
+ // Триггерим нативное change событие для других обработчиков
console.log('12. Триггерим нативное change событие для customer ID:', data.id);
const changeEvent = new Event('change', { bubbles: true });
this.dispatchEvent(changeEvent);
@@ -2181,12 +2203,37 @@ if (!document.getElementById('notification-styles')) {
})();
-
-{% if order %}
-
-
-{% else %}
-
-
-{% endif %}
+
+
{% endblock %}
diff --git a/myproject/orders/urls.py b/myproject/orders/urls.py
index 37e2a50..01d566d 100644
--- a/myproject/orders/urls.py
+++ b/myproject/orders/urls.py
@@ -12,8 +12,6 @@ urlpatterns = [
path('/delete/', views.order_delete, name='order-delete'),
# AJAX endpoints
- path('/autosave/', views.autosave_draft_order, name='order-autosave'),
- path('create-draft/', views.create_draft_from_form, name='order-create-draft'),
path('api/customer-address-history/', views.get_customer_address_history, name='api-customer-address-history'),
# Wallet payment
diff --git a/myproject/orders/views.py b/myproject/orders/views.py
index 3585404..db3273a 100644
--- a/myproject/orders/views.py
+++ b/myproject/orders/views.py
@@ -11,7 +11,6 @@ from decimal import Decimal
from .models import Order, OrderItem, Address, OrderStatus
from .forms import OrderForm, OrderItemFormSet, OrderStatusForm, PaymentFormSet
from .filters import OrderFilter
-from .services import DraftOrderService
from .services.address_service import AddressService
import json
@@ -80,11 +79,15 @@ def order_create(request):
address.save()
order.delivery_address = address
- # Если нажата кнопка "Сохранить как черновик", создаем черновик
+ # Проверяем какая кнопка нажата
if 'save_as_draft' in request.POST:
+ # Кнопка "Сохранить как черновик"
from .services.order_status_service import OrderStatusService
order.status = OrderStatusService.get_draft_status()
order.modified_by = request.user
+ else:
+ # Кнопка "Создать заказ" - статус из формы или NULL
+ order.modified_by = request.user
order.save()
@@ -131,6 +134,7 @@ def order_create(request):
'preselected_customer': preselected_customer,
'title': 'Создание заказа',
'button_text': 'Создать заказ',
+ 'is_create_page': True,
}
return render(request, 'orders/order_form.html', context)
@@ -150,15 +154,29 @@ def order_update(request, order_number):
# Если черновик финализируется
if 'finalize_draft' in request.POST and order.is_draft():
- try:
- order = DraftOrderService.finalize_draft(order.pk, request.user)
- messages.success(request, f'Черновик #{order.order_number} успешно завершен и переведен в статус "Новый"!')
- return redirect('orders:order-detail', order_number=order.order_number)
- except ValidationError as e:
- messages.error(request, f'Ошибка финализации: {str(e)}')
- form = OrderForm(instance=order)
- formset = OrderItemFormSet(instance=order)
- payment_formset = PaymentFormSet(instance=order)
+ from .services.order_status_service import OrderStatusService
+ # Переводим в статус "Новый"
+ order.status = OrderStatusService.get_new_status()
+ order.modified_by = request.user
+
+ # Обрабатываем адрес доставки
+ if order.is_delivery:
+ address = AddressService.process_address_from_form(order, form.cleaned_data)
+ if address:
+ if not address.pk:
+ address.save()
+ order.delivery_address = address
+
+ order.save()
+ formset.save()
+ payment_formset.save()
+
+ # Пересчитываем итоговую сумму
+ order.calculate_total()
+ order.save()
+
+ messages.success(request, f'Черновик #{order.order_number} успешно завершен и переведен в статус "Новый"!')
+ return redirect('orders:order-detail', order_number=order.order_number)
else:
# Обрабатываем адрес доставки
if order.is_delivery:
@@ -255,250 +273,6 @@ def order_delete(request, order_number):
# === AJAX ENDPOINTS ===
@require_http_methods(["POST"])
-@login_required
-def autosave_draft_order(request, order_number):
- """
- AJAX endpoint для автосохранения черновика заказа.
-
- Принимает JSON с данными формы и обновляет черновик.
- Возвращает статус сохранения и время последнего сохранения.
-
- Пример запроса:
- {
- "customer": 1,
- "is_delivery": true,
- "delivery_address": 5,
- "delivery_date": "2024-01-15",
- "special_instructions": "Позвонить за час",
- "items": [
- {"product_id": 10, "quantity": "2", "price": "500"},
- {"product_kit_id": 5, "quantity": "1", "price": "1500"}
- ]
- }
-
- Ответ при успехе:
- {
- "success": true,
- "last_saved": "2024-01-10T15:30:45.123456",
- "order_id": 123,
- "order_number": "ORD-000123"
- }
- """
- try:
- data = json.loads(request.body)
-
- # Проверяем существование заказа
- try:
- order = Order.objects.get(order_number=order_number)
- except Order.DoesNotExist:
- return JsonResponse({
- 'success': False,
- 'error': 'Заказ не найден'
- }, status=404)
-
- # Обновляем основные поля заказа из DraftOrderService (БЕЗ товаров)
- # Товары обрабатываем отдельно ниже
- order_fields_only = {k: v for k, v in data.items() if k not in ['items', 'payments']}
- order = DraftOrderService.update_draft(
- order_id=order.pk,
- user=request.user,
- data=order_fields_only
- )
-
- # Обрабатываем позиции заказа, если они переданы
- if 'items' in data:
- from decimal import Decimal, InvalidOperation
-
- # Получаем ID товаров, которые нужно удалить
- deleted_item_ids = data.get('deleted_item_ids', [])
- if deleted_item_ids:
- order.items.filter(pk__in=deleted_item_ids).delete()
-
- # Обрабатываем каждый товар
- for item_data in data['items']:
- item_id = item_data.get('id') # ID существующего товара (если есть)
- product_id = item_data.get('product_id')
- product_kit_id = item_data.get('product_kit_id')
- quantity = item_data.get('quantity')
- price_raw = item_data.get('price')
-
- # Преобразуем цену
- try:
- price = Decimal(str(price_raw).replace(',', '.')) if price_raw else None
- except (ValueError, InvalidOperation):
- price = None
-
- # Если есть ID - обновляем существующий товар
- if item_id:
- try:
- item = order.items.get(pk=item_id)
- # Обновляем поля
- if product_id:
- from products.models import Product
- item.product = Product.objects.get(pk=product_id)
- item.product_kit = None
- elif product_kit_id:
- from products.models import ProductKit
- item.product_kit = ProductKit.objects.get(pk=product_kit_id)
- item.product = None
-
- if quantity:
- item.quantity = quantity
- if price is not None:
- item.price = price
-
- item.save()
- except OrderItem.DoesNotExist:
- # Если товар не найден, создаем новый
- item_id = None
-
- # Если нет ID - создаем новый товар
- if not item_id:
- if product_id:
- DraftOrderService.add_item_to_draft(
- order_id=order.pk,
- product_id=product_id,
- quantity=quantity,
- price=price
- )
- elif product_kit_id:
- DraftOrderService.add_item_to_draft(
- order_id=order.pk,
- product_kit_id=product_kit_id,
- quantity=quantity,
- price=price
- )
-
- # НЕ ОБРАБАТЫВАЕМ ПЛАТЕЖИ В АВТОСОХРАНЕНИИ
- # Платежи обрабатываются только при ручном сохранении формы
-
- # Пересчитываем итоговую сумму заказа и обновляем статус оплаты
- order.calculate_total()
- order.update_payment_status()
- order.save()
-
- return JsonResponse({
- 'success': True,
- 'last_saved': order.last_autosave_at.isoformat() if order.last_autosave_at else None,
- 'order_id': order.pk,
- 'order_number': order.order_number
- })
-
- except ValidationError as e:
- return JsonResponse({
- 'success': False,
- 'error': str(e)
- }, status=400)
- except json.JSONDecodeError:
- return JsonResponse({
- 'success': False,
- 'error': 'Некорректный JSON'
- }, status=400)
- except Exception as e:
- return JsonResponse({
- 'success': False,
- 'error': f'Ошибка сервера: {str(e)}'
- }, status=500)
-
-
-@require_http_methods(["POST"])
-@login_required
-def create_draft_from_form(request):
- """
- AJAX endpoint для создания черновика заказа из формы создания.
-
- Используется для автоматического создания черновика при первом изменении формы.
- После создания возвращает ID черновика для перенаправления.
-
- Пример запроса:
- {
- "customer": 1,
- "is_delivery": true,
- "delivery_date": "2024-01-15"
- }
-
- Ответ при успехе:
- {
- "success": true,
- "order_id": 123,
- "order_number": "ORD-000123",
- "redirect_url": "/orders/123/edit/"
- }
- """
- try:
- data = json.loads(request.body)
-
- # Получаем обязательное поле - клиента
- customer_id = data.get('customer')
- if not customer_id:
- return JsonResponse({
- 'success': False,
- 'error': 'Необходимо выбрать клиента'
- }, status=400)
-
- from customers.models import Customer
- try:
- customer = Customer.objects.get(pk=customer_id)
- except Customer.DoesNotExist:
- return JsonResponse({
- 'success': False,
- 'error': 'Клиент не найден'
- }, status=404)
-
- # Создаем черновик через DraftOrderService
- order = DraftOrderService.create_draft(
- user=request.user,
- customer=customer,
- data=data
- )
-
- # Обрабатываем позиции заказа, если они переданы
- if 'items' in data:
- for item_data in data['items']:
- product_id = item_data.get('product_id')
- product_kit_id = item_data.get('product_kit_id')
- quantity = item_data.get('quantity')
- price = item_data.get('price')
-
- if product_id:
- DraftOrderService.add_item_to_draft(
- order_id=order.pk,
- product_id=product_id,
- quantity=quantity,
- price=price
- )
- elif product_kit_id:
- DraftOrderService.add_item_to_draft(
- order_id=order.pk,
- product_kit_id=product_kit_id,
- quantity=quantity,
- price=price
- )
-
- return JsonResponse({
- 'success': True,
- 'order_id': order.pk,
- 'order_number': order.order_number,
- 'redirect_url': f'/orders/{order.order_number}/edit/'
- })
-
- except ValidationError as e:
- return JsonResponse({
- 'success': False,
- 'error': str(e)
- }, status=400)
- except json.JSONDecodeError:
- return JsonResponse({
- 'success': False,
- 'error': 'Некорректный JSON'
- }, status=400)
- except Exception as e:
- return JsonResponse({
- 'success': False,
- 'error': f'Ошибка сервера: {str(e)}'
- }, status=500)
-
-
@require_http_methods(["GET"])
@login_required
def get_customer_address_history(request):