Улучшения в моделях заказов и комплектов

## Изменения:

### 1. ProductKit - расчет цены для вариантов товаров
- Добавлена обработка variant_group в методах расчета base_price
- Теперь учитываются варианты товаров при расчете стоимости комплекта

### 2. DraftOrderService - упрощение логики автосохранения
- Удалена проверка is_draft() при обновлении (позволяет обновлять заказы в других статусах)
- Улучшена документация метода update_draft

### 3. Шаблоны и скрипты
- Обновлены шаблоны форм создания/редактирования комплектов
- Обновлены скрипты автосохранения

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-12 11:34:06 +03:00
parent 77064a274f
commit 46578382b0
9 changed files with 203 additions and 68 deletions

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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);
}
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -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) {