feat(pos): enhance product kit price handling and UI interaction
- Updated price aggregation logic in update_product_kit to include unit prices. - Improved terminal.js to allow inline editing of product prices in the kit. - Added parsePrice function for consistent price parsing across the application. - Ensured that the correct price is saved when creating or updating product kits.
This commit is contained in:
@@ -2165,11 +2165,63 @@ function renderTempKitItems() {
|
|||||||
// Левая часть: название и цена
|
// Левая часть: название и цена
|
||||||
const leftDiv = document.createElement('div');
|
const leftDiv = document.createElement('div');
|
||||||
leftDiv.className = 'flex-grow-1';
|
leftDiv.className = 'flex-grow-1';
|
||||||
leftDiv.innerHTML = `
|
|
||||||
<strong class="small">${item.name}</strong>
|
// Название товара
|
||||||
<br>
|
const nameSpan = document.createElement('strong');
|
||||||
<small class="text-muted">${formatMoney(item.price)} руб. / шт.</small>
|
nameSpan.className = 'small';
|
||||||
`;
|
nameSpan.textContent = item.name;
|
||||||
|
leftDiv.appendChild(nameSpan);
|
||||||
|
leftDiv.appendChild(document.createElement('br'));
|
||||||
|
|
||||||
|
// Цена с возможностью редактирования
|
||||||
|
const priceContainer = document.createElement('div');
|
||||||
|
priceContainer.className = 'd-inline-flex align-items-center gap-1';
|
||||||
|
|
||||||
|
// Отображение цены (кликабельное)
|
||||||
|
const priceDisplay = document.createElement('small');
|
||||||
|
priceDisplay.className = 'text-muted price-display';
|
||||||
|
priceDisplay.style.cursor = 'pointer';
|
||||||
|
priceDisplay.innerHTML = `<u>${formatMoney(item.price)}</u> руб. / шт.`;
|
||||||
|
priceDisplay.title = 'Кликните для изменения цены';
|
||||||
|
|
||||||
|
// Поле ввода (скрыто по умолчанию)
|
||||||
|
const priceInput = document.createElement('input');
|
||||||
|
priceInput.type = 'number';
|
||||||
|
priceInput.step = '0.01';
|
||||||
|
priceInput.className = 'form-control form-control-sm';
|
||||||
|
priceInput.style.width = '80px';
|
||||||
|
priceInput.style.display = 'none';
|
||||||
|
priceInput.value = item.price;
|
||||||
|
|
||||||
|
// Клик на цену — показать input
|
||||||
|
priceDisplay.onclick = () => {
|
||||||
|
priceDisplay.style.display = 'none';
|
||||||
|
priceInput.style.display = 'inline-block';
|
||||||
|
priceInput.focus();
|
||||||
|
priceInput.select();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Потеря фокуса или Enter — сохранить и скрыть input
|
||||||
|
const savePrice = () => {
|
||||||
|
const newPrice = parseFloat(priceInput.value) || 0;
|
||||||
|
item.price = newPrice;
|
||||||
|
priceDisplay.innerHTML = `<u>${formatMoney(newPrice)}</u> руб. / шт.`;
|
||||||
|
priceInput.style.display = 'none';
|
||||||
|
priceDisplay.style.display = 'inline';
|
||||||
|
renderTempKitItems(); // Пересчёт итогов
|
||||||
|
};
|
||||||
|
|
||||||
|
priceInput.onblur = savePrice;
|
||||||
|
priceInput.onkeydown = (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
savePrice();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
priceContainer.appendChild(priceInput);
|
||||||
|
priceContainer.appendChild(priceDisplay);
|
||||||
|
leftDiv.appendChild(priceContainer);
|
||||||
|
|
||||||
// Правая часть: контролы количества и удаление
|
// Правая часть: контролы количества и удаление
|
||||||
const rightDiv = document.createElement('div');
|
const rightDiv = document.createElement('div');
|
||||||
|
|||||||
@@ -1371,12 +1371,19 @@ def update_product_kit(request, kit_id):
|
|||||||
if len(products) != len(product_ids):
|
if len(products) != len(product_ids):
|
||||||
return JsonResponse({'success': False, 'error': 'Некоторые товары не найдены'}, status=400)
|
return JsonResponse({'success': False, 'error': 'Некоторые товары не найдены'}, status=400)
|
||||||
|
|
||||||
# Агрегируем количества
|
# Агрегируем количества и цены
|
||||||
aggregated_items = {}
|
aggregated_items = {}
|
||||||
for item in items:
|
for item in items:
|
||||||
product_id = item['product_id']
|
product_id = item['product_id']
|
||||||
quantity = Decimal(str(item['quantity']))
|
quantity = Decimal(str(item['quantity']))
|
||||||
aggregated_items[product_id] = aggregated_items.get(product_id, Decimal('0')) + quantity
|
unit_price = item.get('unit_price')
|
||||||
|
if product_id in aggregated_items:
|
||||||
|
aggregated_items[product_id]['quantity'] += quantity
|
||||||
|
else:
|
||||||
|
aggregated_items[product_id] = {
|
||||||
|
'quantity': quantity,
|
||||||
|
'unit_price': Decimal(str(unit_price)) if unit_price is not None else None
|
||||||
|
}
|
||||||
|
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
# Получаем старый состав для сравнения
|
# Получаем старый состав для сравнения
|
||||||
@@ -1397,7 +1404,7 @@ def update_product_kit(request, kit_id):
|
|||||||
|
|
||||||
for product_id in all_product_ids:
|
for product_id in all_product_ids:
|
||||||
old_qty = old_items.get(product_id, Decimal('0'))
|
old_qty = old_items.get(product_id, Decimal('0'))
|
||||||
new_qty = aggregated_items.get(product_id, Decimal('0'))
|
new_qty = aggregated_items.get(product_id, {}).get('quantity', Decimal('0'))
|
||||||
diff = new_qty - old_qty
|
diff = new_qty - old_qty
|
||||||
|
|
||||||
if diff > 0 and showcase:
|
if diff > 0 and showcase:
|
||||||
@@ -1436,13 +1443,15 @@ def update_product_kit(request, kit_id):
|
|||||||
|
|
||||||
# Обновляем состав
|
# Обновляем состав
|
||||||
kit.kit_items.all().delete()
|
kit.kit_items.all().delete()
|
||||||
for product_id, quantity in aggregated_items.items():
|
for product_id, item_data in aggregated_items.items():
|
||||||
product = products[product_id]
|
product = products[product_id]
|
||||||
|
# Используем переданную цену, если есть, иначе актуальную из каталога
|
||||||
|
final_price = item_data['unit_price'] if item_data['unit_price'] is not None else product.actual_price
|
||||||
KitItem.objects.create(
|
KitItem.objects.create(
|
||||||
kit=kit,
|
kit=kit,
|
||||||
product=product,
|
product=product,
|
||||||
quantity=quantity,
|
quantity=item_data['quantity'],
|
||||||
unit_price=product.actual_price # Фиксируем актуальную цену
|
unit_price=final_price
|
||||||
)
|
)
|
||||||
|
|
||||||
kit.recalculate_base_price()
|
kit.recalculate_base_price()
|
||||||
|
|||||||
@@ -748,6 +748,16 @@
|
|||||||
// Кэш цен товаров для быстрого доступа
|
// Кэш цен товаров для быстрого доступа
|
||||||
const priceCache = {};
|
const priceCache = {};
|
||||||
|
|
||||||
|
function parsePrice(value) {
|
||||||
|
if (value === null || value === undefined) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
return parseFloat(value.replace(',', '.')) || 0;
|
||||||
|
}
|
||||||
|
return parseFloat(value) || 0;
|
||||||
|
}
|
||||||
|
|
||||||
// Функция для получения цены товара с AJAX если необходимо
|
// Функция для получения цены товара с AJAX если необходимо
|
||||||
async function getProductPrice(selectElement) {
|
async function getProductPrice(selectElement) {
|
||||||
// Строгая проверка: нужен валидный element с value
|
// Строгая проверка: нужен валидный element с value
|
||||||
@@ -765,7 +775,7 @@
|
|||||||
|
|
||||||
// Если уже загружена в кэш - возвращаем
|
// Если уже загружена в кэш - возвращаем
|
||||||
if (priceCache[productId] !== undefined) {
|
if (priceCache[productId] !== undefined) {
|
||||||
const cachedPrice = parseFloat(priceCache[productId]) || 0;
|
const cachedPrice = parsePrice(priceCache[productId]);
|
||||||
console.log('getProductPrice: from cache', productId, cachedPrice);
|
console.log('getProductPrice: from cache', productId, cachedPrice);
|
||||||
return cachedPrice;
|
return cachedPrice;
|
||||||
}
|
}
|
||||||
@@ -776,7 +786,7 @@
|
|||||||
const formPrice = form.getAttribute('data-product-price');
|
const formPrice = form.getAttribute('data-product-price');
|
||||||
const formProductId = form.getAttribute('data-product-id');
|
const formProductId = form.getAttribute('data-product-id');
|
||||||
if (formPrice && productId.toString() === formProductId) {
|
if (formPrice && productId.toString() === formProductId) {
|
||||||
const price = parseFloat(formPrice) || 0;
|
const price = parsePrice(formPrice);
|
||||||
if (price > 0) {
|
if (price > 0) {
|
||||||
priceCache[productId] = price;
|
priceCache[productId] = price;
|
||||||
console.log('getProductPrice: from form data', productId, price);
|
console.log('getProductPrice: from form data', productId, price);
|
||||||
@@ -789,7 +799,7 @@
|
|||||||
const selectedOption = $(selectElement).find('option:selected');
|
const selectedOption = $(selectElement).find('option:selected');
|
||||||
let priceData = selectedOption.data('actual_price') || selectedOption.data('price');
|
let priceData = selectedOption.data('actual_price') || selectedOption.data('price');
|
||||||
if (priceData) {
|
if (priceData) {
|
||||||
const price = parseFloat(priceData) || 0;
|
const price = parsePrice(priceData);
|
||||||
if (price > 0) {
|
if (price > 0) {
|
||||||
priceCache[productId] = price;
|
priceCache[productId] = price;
|
||||||
console.log('getProductPrice: from select2 data', productId, price);
|
console.log('getProductPrice: from select2 data', productId, price);
|
||||||
@@ -808,7 +818,7 @@
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.results && data.results.length > 0) {
|
if (data.results && data.results.length > 0) {
|
||||||
const productData = data.results[0];
|
const productData = data.results[0];
|
||||||
const price = parseFloat(productData.actual_price || productData.price || 0);
|
const price = parsePrice(productData.actual_price || productData.price || 0);
|
||||||
if (price > 0) {
|
if (price > 0) {
|
||||||
priceCache[productId] = price;
|
priceCache[productId] = price;
|
||||||
console.log('getProductPrice: from API', productId, price);
|
console.log('getProductPrice: from API', productId, price);
|
||||||
@@ -868,7 +878,7 @@
|
|||||||
// Если уже загружена в кэш - возвращаем
|
// Если уже загружена в кэш - возвращаем
|
||||||
const cacheKey = `variant_${variantGroupId}`;
|
const cacheKey = `variant_${variantGroupId}`;
|
||||||
if (priceCache[cacheKey] !== undefined) {
|
if (priceCache[cacheKey] !== undefined) {
|
||||||
const cachedPrice = parseFloat(priceCache[cacheKey]) || 0;
|
const cachedPrice = parsePrice(priceCache[cacheKey]);
|
||||||
console.log('getVariantGroupPrice: from cache', variantGroupId, cachedPrice);
|
console.log('getVariantGroupPrice: from cache', variantGroupId, cachedPrice);
|
||||||
return cachedPrice;
|
return cachedPrice;
|
||||||
}
|
}
|
||||||
@@ -877,7 +887,7 @@
|
|||||||
const selectedOption = $(selectElement).find('option:selected');
|
const selectedOption = $(selectElement).find('option:selected');
|
||||||
let priceData = selectedOption.data('actual_price') || selectedOption.data('price');
|
let priceData = selectedOption.data('actual_price') || selectedOption.data('price');
|
||||||
if (priceData) {
|
if (priceData) {
|
||||||
const price = parseFloat(priceData) || 0;
|
const price = parsePrice(priceData);
|
||||||
if (price > 0) {
|
if (price > 0) {
|
||||||
priceCache[cacheKey] = price;
|
priceCache[cacheKey] = price;
|
||||||
console.log('getVariantGroupPrice: from select2 data', variantGroupId, price);
|
console.log('getVariantGroupPrice: from select2 data', variantGroupId, price);
|
||||||
@@ -896,7 +906,7 @@
|
|||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
if (data.results && data.results.length > 0) {
|
if (data.results && data.results.length > 0) {
|
||||||
const variantData = data.results[0];
|
const variantData = data.results[0];
|
||||||
const price = parseFloat(variantData.actual_price || variantData.price || 0);
|
const price = parsePrice(variantData.actual_price || variantData.price || 0);
|
||||||
if (price > 0) {
|
if (price > 0) {
|
||||||
priceCache[cacheKey] = price;
|
priceCache[cacheKey] = price;
|
||||||
console.log('getVariantGroupPrice: from API', variantGroupId, price);
|
console.log('getVariantGroupPrice: from API', variantGroupId, price);
|
||||||
@@ -940,7 +950,7 @@
|
|||||||
// Если уже загружена в кэш - возвращаем
|
// Если уже загружена в кэш - возвращаем
|
||||||
const cacheKey = `sales_unit_${salesUnitId}`;
|
const cacheKey = `sales_unit_${salesUnitId}`;
|
||||||
if (priceCache[cacheKey] !== undefined) {
|
if (priceCache[cacheKey] !== undefined) {
|
||||||
const cachedPrice = parseFloat(priceCache[cacheKey]) || 0;
|
const cachedPrice = parsePrice(priceCache[cacheKey]);
|
||||||
return cachedPrice;
|
return cachedPrice;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -949,7 +959,7 @@
|
|||||||
if (selectedOption) {
|
if (selectedOption) {
|
||||||
let priceData = selectedOption.dataset.actual_price || selectedOption.dataset.price;
|
let priceData = selectedOption.dataset.actual_price || selectedOption.dataset.price;
|
||||||
if (priceData) {
|
if (priceData) {
|
||||||
const price = parseFloat(priceData) || 0;
|
const price = parsePrice(priceData);
|
||||||
if (price > 0) {
|
if (price > 0) {
|
||||||
priceCache[cacheKey] = price;
|
priceCache[cacheKey] = price;
|
||||||
console.log('getSalesUnitPrice: from standard select option data', salesUnitId, price);
|
console.log('getSalesUnitPrice: from standard select option data', salesUnitId, price);
|
||||||
@@ -966,7 +976,7 @@
|
|||||||
const itemData = selectedData[0];
|
const itemData = selectedData[0];
|
||||||
const priceData = itemData.actual_price || itemData.price;
|
const priceData = itemData.actual_price || itemData.price;
|
||||||
if (priceData) {
|
if (priceData) {
|
||||||
const price = parseFloat(priceData) || 0;
|
const price = parsePrice(priceData);
|
||||||
if (price > 0) {
|
if (price > 0) {
|
||||||
priceCache[cacheKey] = price;
|
priceCache[cacheKey] = price;
|
||||||
console.log('getSalesUnitPrice: from select2 data', salesUnitId, price);
|
console.log('getSalesUnitPrice: from select2 data', salesUnitId, price);
|
||||||
@@ -988,7 +998,7 @@
|
|||||||
if (data.sales_units && data.sales_units.length > 0) {
|
if (data.sales_units && data.sales_units.length > 0) {
|
||||||
const salesUnitData = data.sales_units.find(su => su.id == salesUnitId);
|
const salesUnitData = data.sales_units.find(su => su.id == salesUnitId);
|
||||||
if (salesUnitData) {
|
if (salesUnitData) {
|
||||||
const price = parseFloat(salesUnitData.actual_price || salesUnitData.price || 0);
|
const price = parsePrice(salesUnitData.actual_price || salesUnitData.price || 0);
|
||||||
if (price > 0) {
|
if (price > 0) {
|
||||||
priceCache[cacheKey] = price;
|
priceCache[cacheKey] = price;
|
||||||
console.log('getSalesUnitPrice: from API', salesUnitId, price);
|
console.log('getSalesUnitPrice: from API', salesUnitId, price);
|
||||||
|
|||||||
Reference in New Issue
Block a user