Добавлено автоматическое создание черновика при создании заказа

При любом изменении формы создания заказа автоматически создается черновик
и пользователь перенаправляется на страницу редактирования, где работает
стандартное автосохранение.

Изменения:
- Добавлен AJAX endpoint create_draft_from_form в views.py
- Создан JavaScript модуль draft-creator.js для автосоздания черновика
- Обновлен order_form.html для подключения соответствующих скриптов
- Добавлен URL маршрут /orders/create-draft/

Работает с задержкой 2 секунды после первого изменения формы.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-08 23:08:28 +03:00
parent 024edea9f1
commit 3ba4ceab3d
4 changed files with 527 additions and 2 deletions

View File

@@ -0,0 +1,422 @@
/**
* Модуль автоматического создания черновика заказа.
*
* При первом изменении формы создания заказа автоматически создаёт черновик
* и перенаправляет пользователя на страницу редактирования черновика,
* где уже работает обычное автосохранение.
*/
(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) {
return;
}
const orderForm = document.querySelector('form[action*="create"]');
if (!orderForm) {
return;
}
console.log('[DraftCreator] Initialized on order create page');
// Добавляем обработчики событий
attachEventListeners();
}
/**
* Прикрепляет обработчики событий к полям формы
*/
function attachEventListeners() {
const form = document.querySelector('form[action*="create"]');
if (!form) {
return;
}
// Слушаем изменения в поле клиента (обязательное поле)
const customerField = form.querySelector('select[name="customer"]');
if (customerField) {
customerField.addEventListener('change', function() {
if (this.value && !draftCreated) {
scheduleCreateDraft();
}
});
}
// Слушаем изменения в других основных полях
const fieldsToWatch = [
'input[name="delivery_date"]',
'input[name="delivery_time_start"]',
'input[name="delivery_time_end"]',
'select[name="payment_method"]',
'textarea[name="special_instructions"]',
'input[type="checkbox"]',
'select[name="delivery_address"]',
'select[name="pickup_shop"]',
];
fieldsToWatch.forEach(selector => {
const fields = form.querySelectorAll(selector);
fields.forEach(field => {
const eventType = (field.tagName === 'SELECT' || field.type === 'checkbox') ? 'change' : 'input';
field.addEventListener(eventType, function() {
// Создаём черновик только если выбран клиент
const customer = form.querySelector('select[name="customer"]');
if (customer && customer.value && !draftCreated) {
scheduleCreateDraft();
}
});
});
});
// Слушаем изменения в формах товаров
observeFormsetChanges();
}
/**
* Наблюдает за изменениями в формсете товаров
*/
function observeFormsetChanges() {
const formsetContainer = document.getElementById('order-items-formset');
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.draftCreatorAttached === 'true') {
return;
}
const fields = form.querySelectorAll('select, input[type="number"]');
fields.forEach(field => {
const eventType = field.tagName === 'SELECT' ? 'change' : 'input';
field.addEventListener(eventType, function() {
const customerField = document.querySelector('select[name="customer"]');
if (customerField && customerField.value && !draftCreated) {
scheduleCreateDraft();
}
});
});
form.dataset.draftCreatorAttached = 'true';
});
}
/**
* Планирует создание черновика с задержкой (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.querySelector('form[action*="create"]');
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 pickupShopField = form.querySelector('select[name="pickup_shop"]');
if (pickupShopField && pickupShopField.value) {
data.pickup_shop = parseInt(pickupShopField.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 = `
<div class="d-flex align-items-center">
<span class="me-2">${type === 'error' ? '⚠️' : ''}</span>
<span>${message}</span>
</div>
`;
document.body.appendChild(notification);
// Автоматически удаляем через 3 секунды
setTimeout(() => {
notification.remove();
}, 3000);
}
// Инициализация при загрузке DOM
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

View File

@@ -885,8 +885,12 @@ document.addEventListener('DOMContentLoaded', function() {
</div> </div>
</div> </div>
<!-- Скрипт автосохранения черновиков --> <!-- Скрипты автосохранения -->
{% if is_draft %} {% if is_draft %}
<!-- Автосохранение для черновиков -->
<script src="{% static 'orders/js/autosave.js' %}"></script> <script src="{% static 'orders/js/autosave.js' %}"></script>
{% else %}
<!-- Автосоздание черновика при создании заказа -->
<script src="{% static 'orders/js/draft-creator.js' %}"></script>
{% endif %} {% endif %}
{% endblock %} {% endblock %}

View File

@@ -13,6 +13,7 @@ urlpatterns = [
# AJAX endpoints # AJAX endpoints
path('<int:pk>/autosave/', views.autosave_draft_order, name='order-autosave'), path('<int:pk>/autosave/', views.autosave_draft_order, name='order-autosave'),
path('create-draft/', views.create_draft_from_form, name='order-create-draft'),
# Временные комплекты # Временные комплекты
path('temporary-kits/create/', views.create_temporary_kit, name='temporary-kit-create'), path('temporary-kits/create/', views.create_temporary_kit, name='temporary-kit-create'),

View File

@@ -284,6 +284,104 @@ def autosave_draft_order(request, pk):
}, status=500) }, 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.pk}/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(["POST"]) @require_http_methods(["POST"])