diff --git a/myproject/customers/templates/customers/customer_detail.html b/myproject/customers/templates/customers/customer_detail.html
index 7b3ecb5..aa7ad78 100644
--- a/myproject/customers/templates/customers/customer_detail.html
+++ b/myproject/customers/templates/customers/customer_detail.html
@@ -126,7 +126,7 @@
{{ transaction.description|default:"-" }} |
{% if transaction.order %}
-
+
#{{ transaction.order.order_number }}
{% else %}
@@ -237,10 +237,10 @@
{% endif %}
|
-
+
-
+
|
diff --git a/myproject/inventory/templates/inventory/reservation/reservation_list.html b/myproject/inventory/templates/inventory/reservation/reservation_list.html
index d7c22e0..460c228 100644
--- a/myproject/inventory/templates/inventory/reservation/reservation_list.html
+++ b/myproject/inventory/templates/inventory/reservation/reservation_list.html
@@ -69,8 +69,8 @@
{% if r.order_item %}
-
- Заказ #{{ r.order_item.order.id }}
+
+ Заказ #{{ r.order_item.order.order_number }}
{% else %}
—
diff --git a/myproject/orders/forms.py b/myproject/orders/forms.py
index ed3c454..6fb01e1 100644
--- a/myproject/orders/forms.py
+++ b/myproject/orders/forms.py
@@ -263,13 +263,15 @@ class OrderItemForm(forms.ModelForm):
self.fields['is_custom_price'].required = False
def clean_price(self):
- """Парсим цену с запятой или точкой"""
+ """Парсим цену с запятой или точкой и округляем до 2 знаков"""
value = self.cleaned_data.get('price')
if value in (None, ''):
return None
value_str = str(value).strip().replace(',', '.')
try:
- return Decimal(value_str)
+ price = Decimal(value_str)
+ # Округляем до 2 знаков после запятой
+ return price.quantize(Decimal('0.01'))
except Exception:
raise forms.ValidationError('Введите число.')
diff --git a/myproject/orders/models/order.py b/myproject/orders/models/order.py
index e9c887b..4d92eaa 100644
--- a/myproject/orders/models/order.py
+++ b/myproject/orders/models/order.py
@@ -248,6 +248,11 @@ class Order(models.Model):
def __str__(self):
return f"Заказ #{self.order_number} - {self.customer}"
+ def get_absolute_url(self):
+ """Возвращает канонический URL для заказа"""
+ from django.urls import reverse
+ return reverse('orders:order-detail', kwargs={'order_number': self.order_number})
+
def save(self, *args, **kwargs):
# Генерируем уникальный номер заказа при создании (начиная с 100 для 3-значного поиска)
if not self.order_number:
diff --git a/myproject/orders/models/payment.py b/myproject/orders/models/payment.py
index 18fa37e..5df762c 100644
--- a/myproject/orders/models/payment.py
+++ b/myproject/orders/models/payment.py
@@ -172,9 +172,11 @@ class Payment(models.Model):
)
# Нормализация переплаты: лишнее в кошелёк, amount_paid = total_amount
- try:
- from customers.services.wallet_service import WalletService
- WalletService.add_overpayment(self.order, self.created_by)
- except Exception:
- # Продолжаем, даже если нормализация переплаты не удалась
- pass
+ # ТОЛЬКО для новых платежей, чтобы избежать дублирования при обновлении
+ if is_new:
+ try:
+ from customers.services.wallet_service import WalletService
+ WalletService.add_overpayment(self.order, self.created_by)
+ except Exception:
+ # Продолжаем, даже если нормализация переплаты не удалась
+ pass
diff --git a/myproject/orders/static/orders/js/autosave.js b/myproject/orders/static/orders/js/autosave.js
index ed1d554..44baf61 100644
--- a/myproject/orders/static/orders/js/autosave.js
+++ b/myproject/orders/static/orders/js/autosave.js
@@ -11,14 +11,14 @@
// Конфигурация
const CONFIG = {
AUTOSAVE_DELAY: 3000, // Задержка перед автосохранением (мс)
- AUTOSAVE_URL_PATTERN: '/orders/{orderId}/autosave/',
+ AUTOSAVE_URL_PATTERN: '/orders/{orderNumber}/autosave/',
STATUS_DISPLAY_DURATION: 5000, // Длительность показа статуса (мс)
};
// Состояние модуля
let autosaveTimer = null;
let isAutosaving = false;
- let orderId = null;
+ let orderNumber = null;
/**
* Инициализация модуля автосохранения
@@ -35,12 +35,12 @@
return;
}
- // Получаем ID заказа из URL
+ // Получаем номер заказа из URL
const urlMatch = window.location.pathname.match(/\/orders\/(\d+)\/edit\//);
if (!urlMatch) {
return;
}
- orderId = urlMatch[1];
+ orderNumber = urlMatch[1];
// Инициализируем UI индикатора
initStatusIndicator();
@@ -314,7 +314,7 @@
const formData = collectFormData();
// Отправляем AJAX запрос
- const url = CONFIG.AUTOSAVE_URL_PATTERN.replace('{orderId}', orderId);
+ const url = CONFIG.AUTOSAVE_URL_PATTERN.replace('{orderNumber}', orderNumber);
const response = await fetch(url, {
method: 'POST',
headers: {
diff --git a/myproject/orders/templates/orders/order_confirm_delete.html b/myproject/orders/templates/orders/order_confirm_delete.html
index ecb2fc9..5e9d812 100644
--- a/myproject/orders/templates/orders/order_confirm_delete.html
+++ b/myproject/orders/templates/orders/order_confirm_delete.html
@@ -50,7 +50,7 @@
|
-
+
{% if order.status %}
{{ order.status.label|default:order.status.name }}
@@ -165,12 +165,12 @@
{% endif %}
|
-
-
@@ -239,9 +239,9 @@
(function() {
const csrfToken = '{{ csrf_token }}';
- async function updateStatus(orderId, statusId) {
+ async function updateStatus(orderNumber, statusId) {
const body = new URLSearchParams({ status_id: statusId }).toString();
- const resp = await fetch(`/orders/api/${orderId}/set-status/`, {
+ const resp = await fetch(`/orders/api/${orderNumber}/set-status/`, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
@@ -255,7 +255,7 @@
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;
+ const orderNumber = container.dataset.orderNumber;
// Click on badge: show select
badge.addEventListener('click', function() {
@@ -278,7 +278,7 @@
select.disabled = true;
try {
- const result = await updateStatus(orderId, statusId);
+ const result = await updateStatus(orderNumber, statusId);
if (result.success) {
// Update badge
const newColor = selectedOption.dataset.color || '#6c757d';
diff --git a/myproject/orders/urls.py b/myproject/orders/urls.py
index 9e202b7..37e2a50 100644
--- a/myproject/orders/urls.py
+++ b/myproject/orders/urls.py
@@ -7,20 +7,20 @@ app_name = 'orders'
urlpatterns = [
path('', views.order_list, name='order-list'),
path('create/', views.order_create, name='order-create'),
- path('/', views.order_detail, name='order-detail'),
- path('/edit/', views.order_update, name='order-update'),
- path('/delete/', views.order_delete, name='order-delete'),
+ path('/', views.order_detail, name='order-detail'),
+ path('/edit/', views.order_update, name='order-update'),
+ path('/delete/', views.order_delete, name='order-delete'),
# AJAX endpoints
- path('/autosave/', views.autosave_draft_order, name='order-autosave'),
+ 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
- path('/apply-wallet/', views.apply_wallet_payment, name='apply-wallet'),
+ path('/apply-wallet/', views.apply_wallet_payment, name='apply-wallet'),
# AJAX status update
- path('api//set-status/', views.set_order_status, name='api-set-order-status'),
+ path('api//set-status/', views.set_order_status, name='api-set-order-status'),
# Order Status Management URLs
path('statuses/', views.order_status_list, name='status_list'),
diff --git a/myproject/orders/views.py b/myproject/orders/views.py
index 550940a..3585404 100644
--- a/myproject/orders/views.py
+++ b/myproject/orders/views.py
@@ -46,12 +46,12 @@ def order_list(request):
return render(request, 'orders/order_list.html', context)
-def order_detail(request, pk):
+def order_detail(request, order_number):
"""Детальная информация о заказе"""
order = get_object_or_404(
Order.objects.select_related('customer', 'delivery_address', 'pickup_warehouse', 'modified_by')
.prefetch_related('items__product', 'items__product_kit', 'payments__created_by'),
- pk=pk
+ order_number=order_number
)
context = {
@@ -104,7 +104,7 @@ def order_create(request):
messages.success(request, f'Черновик #{order.order_number} успешно создан!')
else:
messages.success(request, f'Заказ #{order.order_number} успешно создан!')
- return redirect('orders:order-detail', pk=order.pk)
+ return redirect('orders:order-detail', order_number=order.order_number)
else:
messages.error(request, 'Пожалуйста, исправьте ошибки в форме.')
else:
@@ -136,9 +136,9 @@ def order_create(request):
return render(request, 'orders/order_form.html', context)
-def order_update(request, pk):
+def order_update(request, order_number):
"""Редактирование заказа"""
- order = get_object_or_404(Order, pk=pk)
+ order = get_object_or_404(Order, order_number=order_number)
if request.method == 'POST':
form = OrderForm(request.POST, instance=order)
@@ -153,7 +153,7 @@ def order_update(request, pk):
try:
order = DraftOrderService.finalize_draft(order.pk, request.user)
messages.success(request, f'Черновик #{order.order_number} успешно завершен и переведен в статус "Новый"!')
- return redirect('orders:order-detail', pk=order.pk)
+ return redirect('orders:order-detail', order_number=order.order_number)
except ValidationError as e:
messages.error(request, f'Ошибка финализации: {str(e)}')
form = OrderForm(instance=order)
@@ -200,7 +200,7 @@ def order_update(request, pk):
messages.success(request, f'Черновик #{order.order_number} успешно обновлен!')
else:
messages.success(request, f'Заказ #{order.order_number} успешно обновлен!')
- return redirect('orders:order-detail', pk=order.pk)
+ return redirect('orders:order-detail', order_number=order.order_number)
else:
# Логируем ошибки для отладки
print("\n=== ОШИБКИ ВАЛИДАЦИИ ФОРМЫ ===")
@@ -235,9 +235,9 @@ def order_update(request, pk):
return render(request, 'orders/order_form.html', context)
-def order_delete(request, pk):
+def order_delete(request, order_number):
"""Удаление заказа с подтверждением"""
- order = get_object_or_404(Order, pk=pk)
+ order = get_object_or_404(Order, order_number=order_number)
if request.method == 'POST':
order_number = order.order_number
@@ -256,7 +256,7 @@ def order_delete(request, pk):
@require_http_methods(["POST"])
@login_required
-def autosave_draft_order(request, pk):
+def autosave_draft_order(request, order_number):
"""
AJAX endpoint для автосохранения черновика заказа.
@@ -289,46 +289,93 @@ def autosave_draft_order(request, pk):
# Проверяем существование заказа
try:
- order = Order.objects.get(pk=pk)
+ order = Order.objects.get(order_number=order_number)
except Order.DoesNotExist:
return JsonResponse({
'success': False,
'error': 'Заказ не найден'
}, status=404)
- # Используем DraftOrderService для обновления
+ # Обновляем основные поля заказа из DraftOrderService (БЕЗ товаров)
+ # Товары обрабатываем отдельно ниже
+ order_fields_only = {k: v for k, v in data.items() if k not in ['items', 'payments']}
order = DraftOrderService.update_draft(
- order_id=pk,
+ order_id=order.pk,
user=request.user,
- data=data
+ data=order_fields_only
)
# Обрабатываем позиции заказа, если они переданы
if 'items' in data:
- # Удаляем существующие позиции
- order.items.all().delete()
+ 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 = item_data.get('price')
+ price_raw = 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
- )
+ # Преобразуем цену
+ 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,
@@ -432,7 +479,7 @@ def create_draft_from_form(request):
'success': True,
'order_id': order.pk,
'order_number': order.order_number,
- 'redirect_url': f'/orders/{order.pk}/edit/'
+ 'redirect_url': f'/orders/{order.order_number}/edit/'
})
except ValidationError as e:
@@ -637,15 +684,15 @@ def order_status_delete(request, pk):
# === КОШЕЛЁК КЛИЕНТА ===
@login_required
-def apply_wallet_payment(request, pk):
+def apply_wallet_payment(request, order_number):
"""
Применение оплаты из кошелька клиента к заказу.
Вызывается через POST-запрос с суммой для списания.
"""
if request.method != 'POST':
- return redirect('orders:order-detail', pk=pk)
+ return redirect('orders:order-detail', order_number=order_number)
- order = get_object_or_404(Order, pk=pk)
+ order = get_object_or_404(Order, order_number=order_number)
# Получаем запрашиваемую сумму из формы
try:
@@ -675,19 +722,19 @@ def apply_wallet_payment(request, pk):
except Exception as e:
messages.error(request, f'Ошибка при оплате из кошелька: {str(e)}')
- return redirect('orders:order-detail', pk=pk)
+ return redirect('orders:order-detail', order_number=order.order_number)
@require_http_methods(["POST"])
@login_required
-def set_order_status(request, pk):
+def set_order_status(request, order_number):
"""
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)
+ order = get_object_or_404(Order, order_number=order_number)
status_id = request.POST.get('status_id', '').strip()
# Allow clearing status if empty
diff --git a/myproject/products/templates/products/productkit_make_permanent.html b/myproject/products/templates/products/productkit_make_permanent.html
index 1572862..6d52b68 100644
--- a/myproject/products/templates/products/productkit_make_permanent.html
+++ b/myproject/products/templates/products/productkit_make_permanent.html
@@ -82,7 +82,7 @@
|