Добавлена функциональность редактирования заказов с обновлением резервов товаров
This commit is contained in:
@@ -143,6 +143,7 @@ class OrderForm(forms.ModelForm):
|
|||||||
'class': 'form-select select2',
|
'class': 'form-select select2',
|
||||||
'data-placeholder': 'Выберите адрес доставки'
|
'data-placeholder': 'Выберите адрес доставки'
|
||||||
})
|
})
|
||||||
|
# Адрес доставки не обязателен при редактировании (создаётся из отдельных полей)
|
||||||
self.fields['delivery_address'].required = False
|
self.fields['delivery_address'].required = False
|
||||||
|
|
||||||
self.fields['pickup_warehouse'].widget.attrs.update({
|
self.fields['pickup_warehouse'].widget.attrs.update({
|
||||||
@@ -221,9 +222,16 @@ class OrderForm(forms.ModelForm):
|
|||||||
class OrderItemForm(forms.ModelForm):
|
class OrderItemForm(forms.ModelForm):
|
||||||
"""Форма для позиции заказа"""
|
"""Форма для позиции заказа"""
|
||||||
|
|
||||||
|
# Элегантно переопределяем поле формы, чтобы парсить '277,00' как Decimal
|
||||||
|
price = forms.CharField(
|
||||||
|
required=False,
|
||||||
|
widget=forms.TextInput(attrs={'class': 'form-control', 'step': '0.01', 'min': '0'})
|
||||||
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = OrderItem
|
model = OrderItem
|
||||||
fields = ['product', 'product_kit', 'quantity', 'price', 'is_custom_price']
|
fields = ['product', 'product_kit', 'quantity', 'price', 'is_custom_price']
|
||||||
|
# ВАЖНО: НЕ включаем 'id' в fields - это предотвращает ошибку валидации
|
||||||
widgets = {
|
widgets = {
|
||||||
'quantity': forms.NumberInput(attrs={'min': 1, 'value': 1}),
|
'quantity': forms.NumberInput(attrs={'min': 1, 'value': 1}),
|
||||||
# Скрываем поля product и product_kit - они будут заполняться через JS
|
# Скрываем поля product и product_kit - они будут заполняться через JS
|
||||||
@@ -254,6 +262,17 @@ class OrderItemForm(forms.ModelForm):
|
|||||||
# Поле is_custom_price устанавливается через JS
|
# Поле is_custom_price устанавливается через JS
|
||||||
self.fields['is_custom_price'].required = False
|
self.fields['is_custom_price'].required = False
|
||||||
|
|
||||||
|
def clean_price(self):
|
||||||
|
"""Парсим цену с запятой или точкой"""
|
||||||
|
value = self.cleaned_data.get('price')
|
||||||
|
if value in (None, ''):
|
||||||
|
return None
|
||||||
|
value_str = str(value).strip().replace(',', '.')
|
||||||
|
try:
|
||||||
|
return Decimal(value_str)
|
||||||
|
except Exception:
|
||||||
|
raise forms.ValidationError('Введите число.')
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
"""Валидация: должен быть выбран либо товар, либо комплект (не оба, не ни один)"""
|
"""Валидация: должен быть выбран либо товар, либо комплект (не оба, не ни один)"""
|
||||||
cleaned_data = super().clean()
|
cleaned_data = super().clean()
|
||||||
|
|||||||
@@ -263,23 +263,18 @@ class Order(models.Model):
|
|||||||
"""Валидация модели"""
|
"""Валидация модели"""
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
# Проверка: для доставки обязателен адрес
|
|
||||||
if self.is_delivery and not self.delivery_address:
|
|
||||||
raise ValidationError({
|
|
||||||
'delivery_address': 'Для доставки необходимо указать адрес доставки'
|
|
||||||
})
|
|
||||||
|
|
||||||
# Проверка: для самовывоза обязателен склад
|
# Проверка: для самовывоза обязателен склад
|
||||||
if not self.is_delivery and not self.pickup_warehouse:
|
if not self.is_delivery and not self.pickup_warehouse:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'pickup_warehouse': 'Для самовывоза необходимо выбрать склад'
|
'pickup_warehouse': 'Для самовывоза необходимо выбрать склад'
|
||||||
})
|
})
|
||||||
|
|
||||||
# Проверка: время окончания должно быть позже времени начала
|
# Проверка: время окончания должно быть позже или равно времени начала
|
||||||
|
# Равные времена означают точное время доставки (например, "к 13:00")
|
||||||
if self.delivery_time_start and self.delivery_time_end:
|
if self.delivery_time_start and self.delivery_time_end:
|
||||||
if self.delivery_time_end <= self.delivery_time_start:
|
if self.delivery_time_end < self.delivery_time_start:
|
||||||
raise ValidationError({
|
raise ValidationError({
|
||||||
'delivery_time_end': 'Время окончания должно быть позже времени начала'
|
'delivery_time_end': 'Время окончания не может быть раньше времени начала'
|
||||||
})
|
})
|
||||||
|
|
||||||
def get_delivery_cost(self):
|
def get_delivery_cost(self):
|
||||||
@@ -384,5 +379,8 @@ class Order(models.Model):
|
|||||||
def delivery_time_window(self):
|
def delivery_time_window(self):
|
||||||
"""Временное окно доставки"""
|
"""Временное окно доставки"""
|
||||||
if self.delivery_time_start and self.delivery_time_end:
|
if self.delivery_time_start and self.delivery_time_end:
|
||||||
|
# Если времена равны - это точное время доставки
|
||||||
|
if self.delivery_time_start == self.delivery_time_end:
|
||||||
|
return f"к {self.delivery_time_start.strftime('%H:%M')}"
|
||||||
return f"{self.delivery_time_start.strftime('%H:%M')} - {self.delivery_time_end.strftime('%H:%M')}"
|
return f"{self.delivery_time_start.strftime('%H:%M')} - {self.delivery_time_end.strftime('%H:%M')}"
|
||||||
return "Время не указано"
|
return "Время не указано"
|
||||||
|
|||||||
@@ -843,6 +843,90 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Глобально определяем initOrderItemSelect2, чтобы она была доступна при вызове ниже
|
||||||
|
window.initOrderItemSelect2 = function(element) {
|
||||||
|
console.log('[initOrderItemSelect2] Вызвана для элемента:', element);
|
||||||
|
|
||||||
|
// Проверяем доступность jQuery
|
||||||
|
if (typeof $ === 'undefined') {
|
||||||
|
console.error('[initOrderItemSelect2] jQuery не загружен!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const $element = $(element);
|
||||||
|
const formIndex = element.dataset.formIndex;
|
||||||
|
console.log('[initOrderItemSelect2] formIndex:', formIndex);
|
||||||
|
|
||||||
|
// Проверяем, что функция initProductSelect2 доступна
|
||||||
|
if (typeof window.initProductSelect2 !== 'function') {
|
||||||
|
console.error('[initOrderItemSelect2] window.initProductSelect2 не определена. Убедитесь, что select2-product-search.js загружен.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('[initOrderItemSelect2] Инициализация Select2 через initProductSelect2...');
|
||||||
|
// Инициализируем Select2 с AJAX поиском
|
||||||
|
window.initProductSelect2(
|
||||||
|
element,
|
||||||
|
'all', // Искать и товары, и комплекты
|
||||||
|
'{% url "products:api-search-products-variants" %}'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Обработка выбора элемента
|
||||||
|
$element.on('select2:select', function(e) {
|
||||||
|
// Проверяем наличие params (может не быть при программном вызове)
|
||||||
|
if (!e.params || !e.params.data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = e.params.data;
|
||||||
|
const idParts = data.id.split('_');
|
||||||
|
const type = idParts[0]; // 'product' или 'kit'
|
||||||
|
const id = idParts[1];
|
||||||
|
|
||||||
|
// Найти скрытые поля product и product_kit
|
||||||
|
const form = element.closest('.order-item-form');
|
||||||
|
const productField = form.querySelector('[name$="-product"]');
|
||||||
|
const kitField = form.querySelector('[name$="-product_kit"]');
|
||||||
|
const priceField = form.querySelector('[name$="-price"]');
|
||||||
|
const isCustomPriceField = form.querySelector('[name$="-is_custom_price"]');
|
||||||
|
|
||||||
|
const originalPrice = data.actual_price || data.price || '';
|
||||||
|
|
||||||
|
// Установить значение в правильное поле
|
||||||
|
if (type === 'product') {
|
||||||
|
productField.value = id;
|
||||||
|
kitField.value = '';
|
||||||
|
priceField.value = originalPrice;
|
||||||
|
} else if (type === 'kit') {
|
||||||
|
kitField.value = id;
|
||||||
|
productField.value = '';
|
||||||
|
priceField.value = originalPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сохраняем оригинальную цену в data-атрибуте
|
||||||
|
priceField.dataset.originalPrice = originalPrice;
|
||||||
|
|
||||||
|
// Сбрасываем флаг кастомной цены
|
||||||
|
isCustomPriceField.value = 'false';
|
||||||
|
|
||||||
|
// Скрываем индикатор
|
||||||
|
const badge = form.querySelector('.custom-price-badge');
|
||||||
|
const priceInfo = form.querySelector('.original-price-info');
|
||||||
|
if (badge) badge.style.display = 'none';
|
||||||
|
if (priceInfo) priceInfo.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Очистка при удалении выбора
|
||||||
|
$element.on('select2:clear', function() {
|
||||||
|
const form = element.closest('.order-item-form');
|
||||||
|
form.querySelector('[name$="-product"]').value = '';
|
||||||
|
form.querySelector('[name$="-product_kit"]').value = '';
|
||||||
|
form.querySelector('[name$="-price"]').value = '';
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[initOrderItemSelect2] Инициализация завершена успешно');
|
||||||
|
};
|
||||||
|
|
||||||
// Ждем пока jQuery загрузится
|
// Ждем пока jQuery загрузится
|
||||||
function initCustomerSelect2() {
|
function initCustomerSelect2() {
|
||||||
if (typeof $ === 'undefined') {
|
if (typeof $ === 'undefined') {
|
||||||
@@ -1158,79 +1242,6 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
customerIsRecipientCheckbox.addEventListener('change', toggleRecipientFields);
|
customerIsRecipientCheckbox.addEventListener('change', toggleRecipientFields);
|
||||||
toggleRecipientFields(); // Инициализация при загрузке
|
toggleRecipientFields(); // Инициализация при загрузке
|
||||||
|
|
||||||
// Инициализация Select2 для поиска товаров/комплектов
|
|
||||||
// ВНИМАНИЕ: Эта функция будет вызвана ПОСЛЕ загрузки select2-product-search.js
|
|
||||||
window.initOrderItemSelect2 = function(element) {
|
|
||||||
const $element = $(element);
|
|
||||||
const formIndex = element.dataset.formIndex;
|
|
||||||
|
|
||||||
// Проверяем, что функция initProductSelect2 доступна
|
|
||||||
if (typeof window.initProductSelect2 !== 'function') {
|
|
||||||
console.error('window.initProductSelect2 is not defined. Make sure select2-product-search.js is loaded.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Инициализируем Select2 с AJAX поиском
|
|
||||||
window.initProductSelect2(
|
|
||||||
element,
|
|
||||||
'all', // Искать и товары, и комплекты
|
|
||||||
'{% url "products:api-search-products-variants" %}'
|
|
||||||
);
|
|
||||||
|
|
||||||
// Обработка выбора элемента
|
|
||||||
$element.on('select2:select', function(e) {
|
|
||||||
// Проверяем наличие params (может не быть при программном вызове)
|
|
||||||
if (!e.params || !e.params.data) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = e.params.data;
|
|
||||||
const idParts = data.id.split('_');
|
|
||||||
const type = idParts[0]; // 'product' или 'kit'
|
|
||||||
const id = idParts[1];
|
|
||||||
|
|
||||||
// Найти скрытые поля product и product_kit
|
|
||||||
const form = element.closest('.order-item-form');
|
|
||||||
const productField = form.querySelector('[name$="-product"]');
|
|
||||||
const kitField = form.querySelector('[name$="-product_kit"]');
|
|
||||||
const priceField = form.querySelector('[name$="-price"]');
|
|
||||||
const isCustomPriceField = form.querySelector('[name$="-is_custom_price"]');
|
|
||||||
|
|
||||||
const originalPrice = data.actual_price || data.price || '';
|
|
||||||
|
|
||||||
// Установить значение в правильное поле
|
|
||||||
if (type === 'product') {
|
|
||||||
productField.value = id;
|
|
||||||
kitField.value = '';
|
|
||||||
priceField.value = originalPrice;
|
|
||||||
} else if (type === 'kit') {
|
|
||||||
kitField.value = id;
|
|
||||||
productField.value = '';
|
|
||||||
priceField.value = originalPrice;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Сохраняем оригинальную цену в data-атрибуте
|
|
||||||
priceField.dataset.originalPrice = originalPrice;
|
|
||||||
|
|
||||||
// Сбрасываем флаг кастомной цены
|
|
||||||
isCustomPriceField.value = 'false';
|
|
||||||
|
|
||||||
// Скрываем индикатор
|
|
||||||
const badge = form.querySelector('.custom-price-badge');
|
|
||||||
const priceInfo = form.querySelector('.original-price-info');
|
|
||||||
if (badge) badge.style.display = 'none';
|
|
||||||
if (priceInfo) priceInfo.style.display = 'none';
|
|
||||||
});
|
|
||||||
|
|
||||||
// Очистка при удалении выбора
|
|
||||||
$element.on('select2:clear', function() {
|
|
||||||
const form = element.closest('.order-item-form');
|
|
||||||
form.querySelector('[name$="-product"]').value = '';
|
|
||||||
form.querySelector('[name$="-product_kit"]').value = '';
|
|
||||||
form.querySelector('[name$="-price"]').value = '';
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// === РАСЧЁТ ИТОГОВОЙ СУММЫ ТОВАРОВ ===
|
// === РАСЧЁТ ИТОГОВОЙ СУММЫ ТОВАРОВ ===
|
||||||
function calculateOrderItemsTotal() {
|
function calculateOrderItemsTotal() {
|
||||||
// Собираем все видимые (не удалённые) формы товаров
|
// Собираем все видимые (не удалённые) формы товаров
|
||||||
@@ -1431,26 +1442,10 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
// Инициализируем итоговую сумму при загрузке страницы
|
// Инициализируем итоговую сумму при загрузке страницы
|
||||||
updateOrderItemsTotal();
|
updateOrderItemsTotal();
|
||||||
|
|
||||||
// Валидация перед отправкой
|
// Валидация перед отправкой (убрана обязательность товаров — можно сохранить пустой заказ)
|
||||||
document.getElementById('order-form').addEventListener('submit', function(e) {
|
document.getElementById('order-form').addEventListener('submit', function(e) {
|
||||||
const visibleForms = Array.from(container.querySelectorAll('.order-item-form'))
|
// Валидация отключена — заказ можно сохранить без товаров
|
||||||
.filter(f => !f.classList.contains('deleted'));
|
// Товары можно добавить позже
|
||||||
|
|
||||||
// Проверяем, что есть хотя бы одна позиция с товаром
|
|
||||||
let hasItems = false;
|
|
||||||
visibleForms.forEach(form => {
|
|
||||||
const productField = form.querySelector('[name$="-product"]');
|
|
||||||
const kitField = form.querySelector('[name$="-product_kit"]');
|
|
||||||
if (productField.value || kitField.value) {
|
|
||||||
hasItems = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!hasItems && visibleForms.length > 0) {
|
|
||||||
e.preventDefault();
|
|
||||||
alert('Добавьте хотя бы один товар или комплект в заказ');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// === ВРЕМЕННЫЕ КОМПЛЕКТЫ ===
|
// === ВРЕМЕННЫЕ КОМПЛЕКТЫ ===
|
||||||
@@ -2147,13 +2142,42 @@ if (!document.getElementById('notification-styles')) {
|
|||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Инициализируем все существующие Select2 для товаров после загрузки модуля
|
// Инициализируем все существующие Select2 для товаров после загрузки модуля
|
||||||
(function() {
|
(function initExistingOrderItems() {
|
||||||
if (typeof window.initOrderItemSelect2 === 'function') {
|
console.log('[Order Items] Начало инициализации существующих элементов');
|
||||||
document.querySelectorAll('.select2-order-item').forEach(window.initOrderItemSelect2);
|
console.log('[Order Items] jQuery доступен?', typeof $ !== 'undefined');
|
||||||
console.log('[Order Items] Select2 initialized for existing items');
|
console.log('[Order Items] initOrderItemSelect2 доступен?', typeof window.initOrderItemSelect2 === 'function');
|
||||||
} else {
|
console.log('[Order Items] initProductSelect2 доступен?', typeof window.initProductSelect2 === 'function');
|
||||||
console.error('[Order Items] window.initOrderItemSelect2 is not defined');
|
|
||||||
|
// Проверяем все зависимости
|
||||||
|
if (typeof $ === 'undefined') {
|
||||||
|
console.log('[Order Items] Ожидание загрузки jQuery...');
|
||||||
|
setTimeout(initExistingOrderItems, 100);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (typeof window.initOrderItemSelect2 !== 'function') {
|
||||||
|
console.log('[Order Items] Ожидание инициализации initOrderItemSelect2...');
|
||||||
|
setTimeout(initExistingOrderItems, 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof window.initProductSelect2 !== 'function') {
|
||||||
|
console.log('[Order Items] Ожидание загрузки initProductSelect2 из select2-product-search.js...');
|
||||||
|
setTimeout(initExistingOrderItems, 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Все зависимости готовы
|
||||||
|
console.log('[Order Items] Все зависимости готовы, запуск инициализации...');
|
||||||
|
const items = document.querySelectorAll('.select2-order-item');
|
||||||
|
console.log('[Order Items] Найдено элементов для инициализации:', items.length);
|
||||||
|
|
||||||
|
items.forEach((item, index) => {
|
||||||
|
console.log(`[Order Items] Инициализация элемента ${index + 1}/${items.length}`);
|
||||||
|
window.initOrderItemSelect2(item);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('[Order Items] Инициализация всех существующих элементов завершена');
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -130,13 +130,23 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
<div class="js-status-container" data-order-id="{{ order.pk }}">
|
||||||
|
<span class="badge badge-lg js-status-badge" style="{% if order.status %}background-color: {{ order.status.color }}; color: #fff;{% else %}background-color: #6c757d; color: #fff;{% endif %} cursor: pointer; font-size: 0.9rem; padding: 0.5rem 0.75rem;" title="Кликните для изменения">
|
||||||
{% if order.status %}
|
{% if order.status %}
|
||||||
<span class="badge" style="background-color: {{ order.status.color }}; color: #fff;">
|
|
||||||
{{ order.status.label|default:order.status.name }}
|
{{ order.status.label|default:order.status.name }}
|
||||||
</span>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-secondary">Не установлен</span>
|
Не установлен
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</span>
|
||||||
|
<select class="form-select form-select-sm js-status-select" style="display: none;">
|
||||||
|
<option value="" {% if not order.status_id %}selected{% endif %}>Не установлен</option>
|
||||||
|
{% for s in status_choices %}
|
||||||
|
<option value="{{ s.pk }}" data-color="{{ s.color }}" data-label="{{ s.label|default:s.name }}" {% if order.status_id == s.pk %}selected{% endif %}>
|
||||||
|
{{ s.label|default:s.name }}
|
||||||
|
</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td><strong>{{ order.total_amount }} руб.</strong></td>
|
<td><strong>{{ order.total_amount }} руб.</strong></td>
|
||||||
<td>
|
<td>
|
||||||
@@ -225,4 +235,79 @@
|
|||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
<script src="{% static 'orders/js/date_filter.js' %}"></script>
|
<script src="{% static 'orders/js/date_filter.js' %}"></script>
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
const csrfToken = '{{ csrf_token }}';
|
||||||
|
|
||||||
|
async function updateStatus(orderId, statusId) {
|
||||||
|
const body = new URLSearchParams({ status_id: statusId }).toString();
|
||||||
|
const resp = await fetch(`/orders/api/${orderId}/set-status/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
|
||||||
|
'X-CSRFToken': csrfToken
|
||||||
|
},
|
||||||
|
body
|
||||||
|
});
|
||||||
|
return resp.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll('.js-status-container').forEach(function(container) {
|
||||||
|
const badge = container.querySelector('.js-status-badge');
|
||||||
|
const select = container.querySelector('.js-status-select');
|
||||||
|
const orderId = container.dataset.orderId;
|
||||||
|
|
||||||
|
// Click on badge: show select
|
||||||
|
badge.addEventListener('click', function() {
|
||||||
|
badge.style.display = 'none';
|
||||||
|
select.style.display = 'inline-block';
|
||||||
|
select.focus();
|
||||||
|
// Open dropdown programmatically
|
||||||
|
if (select.showPicker) {
|
||||||
|
select.showPicker();
|
||||||
|
} else {
|
||||||
|
// Fallback for browsers without showPicker
|
||||||
|
select.click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Change status
|
||||||
|
select.addEventListener('change', async function() {
|
||||||
|
const statusId = select.value || '';
|
||||||
|
const selectedOption = select.options[select.selectedIndex];
|
||||||
|
select.disabled = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await updateStatus(orderId, statusId);
|
||||||
|
if (result.success) {
|
||||||
|
// Update badge
|
||||||
|
const newColor = selectedOption.dataset.color || '#6c757d';
|
||||||
|
const newLabel = selectedOption.dataset.label || 'Не установлен';
|
||||||
|
badge.style.backgroundColor = newColor;
|
||||||
|
badge.style.color = '#fff';
|
||||||
|
badge.textContent = newLabel;
|
||||||
|
|
||||||
|
// Show badge, hide select
|
||||||
|
select.style.display = 'none';
|
||||||
|
badge.style.display = 'inline-block';
|
||||||
|
} else {
|
||||||
|
alert(result.error || 'Не удалось обновить статус');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
alert('Ошибка сервера при обновлении статуса');
|
||||||
|
} finally {
|
||||||
|
select.disabled = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click outside or blur: hide select, show badge
|
||||||
|
select.addEventListener('blur', function() {
|
||||||
|
setTimeout(function() {
|
||||||
|
select.style.display = 'none';
|
||||||
|
badge.style.display = 'inline-block';
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ urlpatterns = [
|
|||||||
# Wallet payment
|
# Wallet payment
|
||||||
path('<int:pk>/apply-wallet/', views.apply_wallet_payment, name='apply-wallet'),
|
path('<int:pk>/apply-wallet/', views.apply_wallet_payment, name='apply-wallet'),
|
||||||
|
|
||||||
|
# AJAX status update
|
||||||
|
path('api/<int:pk>/set-status/', views.set_order_status, name='api-set-order-status'),
|
||||||
|
|
||||||
# Order Status Management URLs
|
# Order Status Management URLs
|
||||||
path('statuses/', views.order_status_list, name='status_list'),
|
path('statuses/', views.order_status_list, name='status_list'),
|
||||||
path('statuses/create/', views.order_status_create, name='status_create'),
|
path('statuses/create/', views.order_status_create, name='status_create'),
|
||||||
|
|||||||
@@ -202,6 +202,20 @@ def order_update(request, pk):
|
|||||||
messages.success(request, f'Заказ #{order.order_number} успешно обновлен!')
|
messages.success(request, f'Заказ #{order.order_number} успешно обновлен!')
|
||||||
return redirect('orders:order-detail', pk=order.pk)
|
return redirect('orders:order-detail', pk=order.pk)
|
||||||
else:
|
else:
|
||||||
|
# Логируем ошибки для отладки
|
||||||
|
print("\n=== ОШИБКИ ВАЛИДАЦИИ ФОРМЫ ===")
|
||||||
|
if not form.is_valid():
|
||||||
|
print(f"OrderForm errors: {form.errors}")
|
||||||
|
if not formset.is_valid():
|
||||||
|
print(f"OrderItemFormSet errors: {formset.errors}")
|
||||||
|
print(f"OrderItemFormSet non_form_errors: {formset.non_form_errors()}")
|
||||||
|
for i, item_form in enumerate(formset):
|
||||||
|
if item_form.errors:
|
||||||
|
print(f" Item form {i} errors: {item_form.errors}")
|
||||||
|
if not payment_formset.is_valid():
|
||||||
|
print(f"PaymentFormSet errors: {payment_formset.errors}")
|
||||||
|
print(f"PaymentFormSet non_form_errors: {payment_formset.non_form_errors()}")
|
||||||
|
print("=== КОНЕЦ ОШИБОК ===\n")
|
||||||
messages.error(request, 'Пожалуйста, исправьте ошибки в форме.')
|
messages.error(request, 'Пожалуйста, исправьте ошибки в форме.')
|
||||||
else:
|
else:
|
||||||
form = OrderForm(instance=order)
|
form = OrderForm(instance=order)
|
||||||
@@ -662,3 +676,43 @@ def apply_wallet_payment(request, pk):
|
|||||||
messages.error(request, f'Ошибка при оплате из кошелька: {str(e)}')
|
messages.error(request, f'Ошибка при оплате из кошелька: {str(e)}')
|
||||||
|
|
||||||
return redirect('orders:order-detail', pk=pk)
|
return redirect('orders:order-detail', pk=pk)
|
||||||
|
|
||||||
|
|
||||||
|
@require_http_methods(["POST"])
|
||||||
|
@login_required
|
||||||
|
def set_order_status(request, pk):
|
||||||
|
"""
|
||||||
|
Update order status via AJAX.
|
||||||
|
Accepts POST with 'status_id' (can be empty to clear).
|
||||||
|
Returns JSON with the resulting status info.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
order = get_object_or_404(Order, pk=pk)
|
||||||
|
status_id = request.POST.get('status_id', '').strip()
|
||||||
|
|
||||||
|
# Allow clearing status if empty
|
||||||
|
if status_id == '':
|
||||||
|
order.status = None
|
||||||
|
order.modified_by = request.user
|
||||||
|
order.save(update_fields=['status', 'modified_by', 'updated_at'])
|
||||||
|
return JsonResponse({'success': True, 'status': None})
|
||||||
|
|
||||||
|
try:
|
||||||
|
status = OrderStatus.objects.get(pk=status_id)
|
||||||
|
except OrderStatus.DoesNotExist:
|
||||||
|
return JsonResponse({'success': False, 'error': 'Status not found'}, status=404)
|
||||||
|
|
||||||
|
order.status = status
|
||||||
|
order.modified_by = request.user
|
||||||
|
order.save(update_fields=['status', 'modified_by', 'updated_at'])
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'success': True,
|
||||||
|
'status': {
|
||||||
|
'id': status.pk,
|
||||||
|
'name': status.label or status.name,
|
||||||
|
'color': status.color
|
||||||
|
}
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return JsonResponse({'success': False, 'error': f'Server error: {str(e)}'}, status=500)
|
||||||
|
|||||||
Reference in New Issue
Block a user