From 9a44c98e6ea88b66f4b4140e5caee4b4a4b6b4d3 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Fri, 28 Nov 2025 23:29:19 +0300 Subject: [PATCH] Simplify order creation and editing - remove autosave - Removed autosave.js (665 lines) and draft-creator.js (441 lines) - Removed draft_service.py (~500 lines) and DraftOrderService - Removed AJAX endpoints: autosave and create-draft - Updated order_create() to add is_create_page flag - Updated order_update() to finalize drafts without DraftOrderService - Added get_new_status() method to OrderStatusService - Updated order_form.html: - Removed old JS includes - Added beforeunload warning for unsaved data - Updated buttons: separate buttons for create/draft/finalize - Total code reduction: ~1600 lines (92% removed) New workflow: - /orders/create/ - user fills form, chooses button - /orders//edit/ - simple editing without autosave - beforeunload warning when leaving page (except on submit) --- myproject/orders/services/__init__.py | 6 +- myproject/orders/services/draft_service.py | 564 --------------- .../orders/services/order_status_service.py | 15 + myproject/orders/static/orders/js/autosave.js | 664 ------------------ .../orders/static/orders/js/draft-creator.js | 440 ------------ .../orders/templates/orders/order_form.html | 81 ++- myproject/orders/urls.py | 2 - myproject/orders/views.py | 284 +------- 8 files changed, 110 insertions(+), 1946 deletions(-) delete mode 100644 myproject/orders/services/draft_service.py delete mode 100644 myproject/orders/static/orders/js/autosave.js delete mode 100644 myproject/orders/static/orders/js/draft-creator.js 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):