From 46578382b085e6162696754580eedcba2328b83d Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Wed, 12 Nov 2025 11:34:06 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A3=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=B2=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D1=8F?= =?UTF-8?q?=D1=85=20=D0=B7=D0=B0=D0=BA=D0=B0=D0=B7=D0=BE=D0=B2=20=D0=B8=20?= =?UTF-8?q?=D0=BA=D0=BE=D0=BC=D0=BF=D0=BB=D0=B5=D0=BA=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Изменения: ### 1. ProductKit - расчет цены для вариантов товаров - Добавлена обработка variant_group в методах расчета base_price - Теперь учитываются варианты товаров при расчете стоимости комплекта ### 2. DraftOrderService - упрощение логики автосохранения - Удалена проверка is_draft() при обновлении (позволяет обновлять заказы в других статусах) - Улучшена документация метода update_draft ### 3. Шаблоны и скрипты - Обновлены шаблоны форм создания/редактирования комплектов - Обновлены скрипты автосохранения 🤖 Generated with Claude Code Co-Authored-By: Claude --- myproject/orders/services/draft_service.py | 7 +- myproject/orders/static/orders/js/autosave.js | 26 ------ .../orders/templates/orders/order_form.html | 28 ++++--- myproject/orders/views.py | 7 -- myproject/products/models/kits.py | 8 ++ .../includes/select2-product-init.html | 10 +++ .../templates/products/productkit_create.html | 82 +++++++++++++++++-- .../templates/products/productkit_edit.html | 82 +++++++++++++++++-- .../templates/products/variantgroup_form.html | 21 ++++- 9 files changed, 203 insertions(+), 68 deletions(-) diff --git a/myproject/orders/services/draft_service.py b/myproject/orders/services/draft_service.py index 7e60dd3..920d6c8 100644 --- a/myproject/orders/services/draft_service.py +++ b/myproject/orders/services/draft_service.py @@ -64,7 +64,7 @@ class DraftOrderService: @staticmethod def update_draft(order_id, user, data): """ - Обновляет существующий черновик заказа. + Обновляет существующий заказ (автосохранение). Args: order_id (int): ID заказа @@ -76,14 +76,11 @@ class DraftOrderService: Raises: Order.DoesNotExist: Если заказ не найден - ValidationError: Если заказ не является черновиком или данные невалидны + ValidationError: Если данные невалидны """ with transaction.atomic(): order = Order.objects.select_for_update().get(pk=order_id) - if not order.is_draft(): - raise ValidationError("Можно обновлять только черновики заказов") - # Обновляем только переданные поля # ForeignKey поля требуют специальной обработки fk_fields = { diff --git a/myproject/orders/static/orders/js/autosave.js b/myproject/orders/static/orders/js/autosave.js index f985a29..dfd567b 100644 --- a/myproject/orders/static/orders/js/autosave.js +++ b/myproject/orders/static/orders/js/autosave.js @@ -18,7 +18,6 @@ // Состояние модуля let autosaveTimer = null; let isAutosaving = false; - let isDraft = false; let orderId = null; /** @@ -43,12 +42,6 @@ } orderId = urlMatch[1]; - // Проверяем, является ли заказ черновиком - isDraft = checkIfDraft(); - if (!isDraft) { - return; - } - // Инициализируем UI индикатора initStatusIndicator(); @@ -56,25 +49,6 @@ attachEventListeners(); } - /** - * Проверяет, является ли заказ черновиком - */ - function checkIfDraft() { - // Проверяем через data-атрибут на форме - const form = document.getElementById('order-form'); - if (form && form.dataset.isDraft === 'true') { - return true; - } - - // Проверяем через заголовок страницы - const title = document.querySelector('h1'); - if (title && title.textContent.includes('черновик')) { - return true; - } - - return false; - } - /** * Создает индикатор статуса автосохранения */ diff --git a/myproject/orders/templates/orders/order_form.html b/myproject/orders/templates/orders/order_form.html index 213c2a3..3363462 100644 --- a/myproject/orders/templates/orders/order_form.html +++ b/myproject/orders/templates/orders/order_form.html @@ -102,15 +102,6 @@

{{ title }}

-
- -
- {{ form.status }} - {% if form.status.errors %} -
{{ form.status.errors }}
- {% endif %} -
-
+
+
+ + {{ form.status }} + {% if form.status.errors %} +
{{ form.status.errors }}
+ {% endif %} +
+
@@ -1674,12 +1676,12 @@ if (!document.getElementById('notification-styles')) { - -{% if is_draft %} - + +{% if order %} + {% else %} - + {% endif %} {% endblock %} diff --git a/myproject/orders/views.py b/myproject/orders/views.py index 99e7528..91e3281 100644 --- a/myproject/orders/views.py +++ b/myproject/orders/views.py @@ -252,13 +252,6 @@ def autosave_draft_order(request, pk): 'error': 'Заказ не найден' }, status=404) - # Проверяем, что это черновик - if not order.is_draft(): - return JsonResponse({ - 'success': False, - 'error': 'Можно автосохранять только черновики' - }, status=400) - # Используем DraftOrderService для обновления order = DraftOrderService.update_draft( order_id=pk, diff --git a/myproject/products/models/kits.py b/myproject/products/models/kits.py index ea4b7c5..733412a 100644 --- a/myproject/products/models/kits.py +++ b/myproject/products/models/kits.py @@ -135,6 +135,10 @@ class ProductKit(BaseProductEntity): actual_price = item.product.actual_price or Decimal('0') qty = item.quantity or Decimal('1') total += actual_price * qty + elif item.variant_group: + actual_price = item.variant_group.price or Decimal('0') + qty = item.quantity or Decimal('1') + total += actual_price * qty self.base_price = total # Обновляем финальную цену @@ -178,6 +182,10 @@ class ProductKit(BaseProductEntity): actual_price = item.product.actual_price or Decimal('0') qty = item.quantity or Decimal('1') total += actual_price * qty + elif item.variant_group: + actual_price = item.variant_group.price or Decimal('0') + qty = item.quantity or Decimal('1') + total += actual_price * qty self.base_price = total # Устанавливаем финальную цену в поле price diff --git a/myproject/products/templates/products/includes/select2-product-init.html b/myproject/products/templates/products/includes/select2-product-init.html index b5b1760..c6ea803 100644 --- a/myproject/products/templates/products/includes/select2-product-init.html +++ b/myproject/products/templates/products/includes/select2-product-init.html @@ -21,6 +21,16 @@ function formatSelectSelection(item) { if (!item.id) return item.text; + // Сохраняем данные о цене в атрибуты DOM элемента для доступа через jQuery .data() + if (item.element) { + if (item.price !== undefined) { + $(item.element).attr('data-price', item.price); + } + if (item.actual_price !== undefined) { + $(item.element).attr('data-actual_price', item.actual_price); + } + } + // Показываем только текст при выборе, цена будет обновляться в JavaScript return item.text || item.id; } diff --git a/myproject/products/templates/products/productkit_create.html b/myproject/products/templates/products/productkit_create.html index 6343dde..72ec6fe 100644 --- a/myproject/products/templates/products/productkit_create.html +++ b/myproject/products/templates/products/productkit_create.html @@ -545,6 +545,67 @@ document.addEventListener('DOMContentLoaded', function() { calculateFinalPrice(); }); + // Функция для получения цены группы вариантов + async function getVariantGroupPrice(selectElement) { + if (!selectElement) { + console.warn('getVariantGroupPrice: selectElement is null or undefined'); + return 0; + } + + const variantGroupId = parseInt(selectElement.value); + + if (!selectElement.value || isNaN(variantGroupId) || variantGroupId <= 0) { + console.warn('getVariantGroupPrice: no valid variant group id', selectElement.value); + return 0; + } + + // Если уже загружена в кэш - возвращаем + const cacheKey = `variant_${variantGroupId}`; + if (priceCache[cacheKey] !== undefined) { + const cachedPrice = parseFloat(priceCache[cacheKey]) || 0; + console.log('getVariantGroupPrice: from cache', variantGroupId, cachedPrice); + return cachedPrice; + } + + // Пытаемся получить из Select2 option data + const selectedOption = $(selectElement).find('option:selected'); + let priceData = selectedOption.data('actual_price') || selectedOption.data('price'); + if (priceData) { + const price = parseFloat(priceData) || 0; + if (price > 0) { + priceCache[cacheKey] = price; + console.log('getVariantGroupPrice: from select2 data', variantGroupId, price); + return price; + } + } + + // Если не нашли - загружаем через AJAX + try { + console.log('getVariantGroupPrice: fetching from API', variantGroupId); + const response = await fetch( + `{% url "products:api-search-products-variants" %}?id=variant_${variantGroupId}`, + { method: 'GET', headers: { 'Accept': 'application/json' } } + ); + if (response.ok) { + const data = await response.json(); + if (data.results && data.results.length > 0) { + const variantData = data.results[0]; + const price = parseFloat(variantData.actual_price || variantData.price || 0); + if (price > 0) { + priceCache[cacheKey] = price; + console.log('getVariantGroupPrice: from API', variantGroupId, price); + } + return price; + } + } + } catch (error) { + console.error('Error fetching variant group price:', error); + } + + console.warn('getVariantGroupPrice: returning 0 for variant group', variantGroupId); + return 0; + } + // Функция для расчета финальной цены async function calculateFinalPrice() { // Получаем базовую цену (сумма всех компонентов) @@ -555,21 +616,28 @@ document.addEventListener('DOMContentLoaded', function() { const forms = formsContainer.querySelectorAll('.kititem-form'); for (const form of forms) { const productSelect = form.querySelector('[name$="-product"]'); + const variantGroupSelect = form.querySelector('[name$="-variant_group"]'); const quantity = parseFloat(form.querySelector('[name$="-quantity"]')?.value || '1'); const deleteCheckbox = form.querySelector('[name$="-DELETE"]'); if (deleteCheckbox && deleteCheckbox.checked) continue; - // Пропускаем если товар не выбран - if (!productSelect || !productSelect.value) continue; - // Пропускаем если количество не валидно const validQuantity = quantity > 0 ? quantity : 1; - // Получаем цену товара (асинхронно) - const productPrice = await getProductPrice(productSelect); - if (productPrice > 0) { - newBasePrice += (productPrice * validQuantity); + // Проверяем товар + if (productSelect && productSelect.value) { + const productPrice = await getProductPrice(productSelect); + if (productPrice > 0) { + newBasePrice += (productPrice * validQuantity); + } + } + // Проверяем группу вариантов + else if (variantGroupSelect && variantGroupSelect.value) { + const variantPrice = await getVariantGroupPrice(variantGroupSelect); + if (variantPrice > 0) { + newBasePrice += (variantPrice * validQuantity); + } } } } diff --git a/myproject/products/templates/products/productkit_edit.html b/myproject/products/templates/products/productkit_edit.html index 98e9fe1..f579276 100644 --- a/myproject/products/templates/products/productkit_edit.html +++ b/myproject/products/templates/products/productkit_edit.html @@ -526,6 +526,67 @@ document.addEventListener('DOMContentLoaded', function() { calculateFinalPrice(); }); + // Функция для получения цены группы вариантов + async function getVariantGroupPrice(selectElement) { + if (!selectElement) { + console.warn('getVariantGroupPrice: selectElement is null or undefined'); + return 0; + } + + const variantGroupId = parseInt(selectElement.value); + + if (!selectElement.value || isNaN(variantGroupId) || variantGroupId <= 0) { + console.warn('getVariantGroupPrice: no valid variant group id', selectElement.value); + return 0; + } + + // Если уже загружена в кэш - возвращаем + const cacheKey = `variant_${variantGroupId}`; + if (priceCache[cacheKey] !== undefined) { + const cachedPrice = parseFloat(priceCache[cacheKey]) || 0; + console.log('getVariantGroupPrice: from cache', variantGroupId, cachedPrice); + return cachedPrice; + } + + // Пытаемся получить из Select2 option data + const selectedOption = $(selectElement).find('option:selected'); + let priceData = selectedOption.data('actual_price') || selectedOption.data('price'); + if (priceData) { + const price = parseFloat(priceData) || 0; + if (price > 0) { + priceCache[cacheKey] = price; + console.log('getVariantGroupPrice: from select2 data', variantGroupId, price); + return price; + } + } + + // Если не нашли - загружаем через AJAX + try { + console.log('getVariantGroupPrice: fetching from API', variantGroupId); + const response = await fetch( + `{% url "products:api-search-products-variants" %}?id=variant_${variantGroupId}`, + { method: 'GET', headers: { 'Accept': 'application/json' } } + ); + if (response.ok) { + const data = await response.json(); + if (data.results && data.results.length > 0) { + const variantData = data.results[0]; + const price = parseFloat(variantData.actual_price || variantData.price || 0); + if (price > 0) { + priceCache[cacheKey] = price; + console.log('getVariantGroupPrice: from API', variantGroupId, price); + } + return price; + } + } + } catch (error) { + console.error('Error fetching variant group price:', error); + } + + console.warn('getVariantGroupPrice: returning 0 for variant group', variantGroupId); + return 0; + } + // Функция для расчета финальной цены async function calculateFinalPrice() { // Получаем базовую цену (сумма всех компонентов) @@ -536,21 +597,28 @@ document.addEventListener('DOMContentLoaded', function() { const forms = formsContainer.querySelectorAll('.kititem-form'); for (const form of forms) { const productSelect = form.querySelector('[name$="-product"]'); + const variantGroupSelect = form.querySelector('[name$="-variant_group"]'); const quantity = parseFloat(form.querySelector('[name$="-quantity"]')?.value || '1'); const deleteCheckbox = form.querySelector('[name$="-DELETE"]'); if (deleteCheckbox && deleteCheckbox.checked) continue; - // Пропускаем если товар не выбран - if (!productSelect || !productSelect.value) continue; - // Пропускаем если количество не валидно const validQuantity = quantity > 0 ? quantity : 1; - // Получаем цену товара (асинхронно) - const productPrice = await getProductPrice(productSelect); - if (productPrice > 0) { - newBasePrice += (productPrice * validQuantity); + // Проверяем товар + if (productSelect && productSelect.value) { + const productPrice = await getProductPrice(productSelect); + if (productPrice > 0) { + newBasePrice += (productPrice * validQuantity); + } + } + // Проверяем группу вариантов + else if (variantGroupSelect && variantGroupSelect.value) { + const variantPrice = await getVariantGroupPrice(variantGroupSelect); + if (variantPrice > 0) { + newBasePrice += (variantPrice * validQuantity); + } } } } diff --git a/myproject/products/templates/products/variantgroup_form.html b/myproject/products/templates/products/variantgroup_form.html index 5fb9f70..c0e0c6d 100644 --- a/myproject/products/templates/products/variantgroup_form.html +++ b/myproject/products/templates/products/variantgroup_form.html @@ -222,7 +222,21 @@ document.addEventListener('DOMContentLoaded', function() { window.initProductSelect2(productSelect, 'product', apiUrl); // Обработчик события при выборе товара - productSelect.addEventListener('select2:select', function(e) { + $(productSelect).on('select2:select', function(e) { + // Извлекаем числовой ID из формата "product_123" + let selectedValue = e.params.data.id; + if (typeof selectedValue === 'string' && selectedValue.startsWith('product_')) { + selectedValue = selectedValue.replace('product_', ''); + + // Создаем новую опцию с правильным ID + const selectedOption = $(this).find('option[value="' + e.params.data.id + '"]'); + if (selectedOption.length > 0) { + // Обновляем существующую опцию + selectedOption.val(selectedValue); + // Устанавливаем новое значение + $(this).val(selectedValue); + } + } updateRowData(row); }); } @@ -246,8 +260,9 @@ document.addEventListener('DOMContentLoaded', function() { const skuMatch = text.match(/\(([^)]+)\)$/); const sku = skuMatch ? skuMatch[1] : '-'; - // Получаем цену и статус через AJAX - fetch(`{% url "products:api-search-products-variants" %}?id=${productSelect.value}`) + // Получаем цену и статус через AJAX (используем числовой ID) + const numericId = productSelect.value; + fetch(`{% url "products:api-search-products-variants" %}?id=${numericId}`) .then(response => response.json()) .then(data => { if (data.results && data.results.length > 0) {