From 2aed6f60e5211e4a443f3a74624eb4f68c6d9bd9 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Tue, 27 Jan 2026 11:31:22 +0300 Subject: [PATCH] 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. --- myproject/pos/static/pos/js/terminal.js | 62 +++++++++++++++++-- myproject/pos/views.py | 21 +++++-- .../templates/products/productkit_edit.html | 32 ++++++---- 3 files changed, 93 insertions(+), 22 deletions(-) diff --git a/myproject/pos/static/pos/js/terminal.js b/myproject/pos/static/pos/js/terminal.js index 8d94620..573fdbb 100644 --- a/myproject/pos/static/pos/js/terminal.js +++ b/myproject/pos/static/pos/js/terminal.js @@ -2165,11 +2165,63 @@ function renderTempKitItems() { // Левая часть: название и цена const leftDiv = document.createElement('div'); leftDiv.className = 'flex-grow-1'; - leftDiv.innerHTML = ` - ${item.name} -
- ${formatMoney(item.price)} руб. / шт. - `; + + // Название товара + const nameSpan = document.createElement('strong'); + 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 = `${formatMoney(item.price)} руб. / шт.`; + 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 = `${formatMoney(newPrice)} руб. / шт.`; + 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'); diff --git a/myproject/pos/views.py b/myproject/pos/views.py index cdd39bf..b227d74 100644 --- a/myproject/pos/views.py +++ b/myproject/pos/views.py @@ -1371,12 +1371,19 @@ def update_product_kit(request, kit_id): if len(products) != len(product_ids): return JsonResponse({'success': False, 'error': 'Некоторые товары не найдены'}, status=400) - # Агрегируем количества + # Агрегируем количества и цены aggregated_items = {} for item in items: product_id = item['product_id'] 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(): # Получаем старый состав для сравнения @@ -1397,7 +1404,7 @@ def update_product_kit(request, kit_id): for product_id in all_product_ids: 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 if diff > 0 and showcase: @@ -1436,13 +1443,15 @@ def update_product_kit(request, kit_id): # Обновляем состав 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] + # Используем переданную цену, если есть, иначе актуальную из каталога + final_price = item_data['unit_price'] if item_data['unit_price'] is not None else product.actual_price KitItem.objects.create( kit=kit, product=product, - quantity=quantity, - unit_price=product.actual_price # Фиксируем актуальную цену + quantity=item_data['quantity'], + unit_price=final_price ) kit.recalculate_base_price() diff --git a/myproject/products/templates/products/productkit_edit.html b/myproject/products/templates/products/productkit_edit.html index 033e13c..ccf91c6 100644 --- a/myproject/products/templates/products/productkit_edit.html +++ b/myproject/products/templates/products/productkit_edit.html @@ -748,6 +748,16 @@ // Кэш цен товаров для быстрого доступа 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 если необходимо async function getProductPrice(selectElement) { // Строгая проверка: нужен валидный element с value @@ -765,7 +775,7 @@ // Если уже загружена в кэш - возвращаем if (priceCache[productId] !== undefined) { - const cachedPrice = parseFloat(priceCache[productId]) || 0; + const cachedPrice = parsePrice(priceCache[productId]); console.log('getProductPrice: from cache', productId, cachedPrice); return cachedPrice; } @@ -776,7 +786,7 @@ const formPrice = form.getAttribute('data-product-price'); const formProductId = form.getAttribute('data-product-id'); if (formPrice && productId.toString() === formProductId) { - const price = parseFloat(formPrice) || 0; + const price = parsePrice(formPrice); if (price > 0) { priceCache[productId] = price; console.log('getProductPrice: from form data', productId, price); @@ -789,7 +799,7 @@ const selectedOption = $(selectElement).find('option:selected'); let priceData = selectedOption.data('actual_price') || selectedOption.data('price'); if (priceData) { - const price = parseFloat(priceData) || 0; + const price = parsePrice(priceData); if (price > 0) { priceCache[productId] = price; console.log('getProductPrice: from select2 data', productId, price); @@ -808,7 +818,7 @@ const data = await response.json(); if (data.results && data.results.length > 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) { priceCache[productId] = price; console.log('getProductPrice: from API', productId, price); @@ -868,7 +878,7 @@ // Если уже загружена в кэш - возвращаем const cacheKey = `variant_${variantGroupId}`; if (priceCache[cacheKey] !== undefined) { - const cachedPrice = parseFloat(priceCache[cacheKey]) || 0; + const cachedPrice = parsePrice(priceCache[cacheKey]); console.log('getVariantGroupPrice: from cache', variantGroupId, cachedPrice); return cachedPrice; } @@ -877,7 +887,7 @@ const selectedOption = $(selectElement).find('option:selected'); let priceData = selectedOption.data('actual_price') || selectedOption.data('price'); if (priceData) { - const price = parseFloat(priceData) || 0; + const price = parsePrice(priceData); if (price > 0) { priceCache[cacheKey] = price; console.log('getVariantGroupPrice: from select2 data', variantGroupId, price); @@ -896,7 +906,7 @@ 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); + const price = parsePrice(variantData.actual_price || variantData.price || 0); if (price > 0) { priceCache[cacheKey] = price; console.log('getVariantGroupPrice: from API', variantGroupId, price); @@ -940,7 +950,7 @@ // Если уже загружена в кэш - возвращаем const cacheKey = `sales_unit_${salesUnitId}`; if (priceCache[cacheKey] !== undefined) { - const cachedPrice = parseFloat(priceCache[cacheKey]) || 0; + const cachedPrice = parsePrice(priceCache[cacheKey]); return cachedPrice; } @@ -949,7 +959,7 @@ if (selectedOption) { let priceData = selectedOption.dataset.actual_price || selectedOption.dataset.price; if (priceData) { - const price = parseFloat(priceData) || 0; + const price = parsePrice(priceData); if (price > 0) { priceCache[cacheKey] = price; console.log('getSalesUnitPrice: from standard select option data', salesUnitId, price); @@ -966,7 +976,7 @@ const itemData = selectedData[0]; const priceData = itemData.actual_price || itemData.price; if (priceData) { - const price = parseFloat(priceData) || 0; + const price = parsePrice(priceData); if (price > 0) { priceCache[cacheKey] = price; console.log('getSalesUnitPrice: from select2 data', salesUnitId, price); @@ -988,7 +998,7 @@ if (data.sales_units && data.sales_units.length > 0) { const salesUnitData = data.sales_units.find(su => su.id == salesUnitId); if (salesUnitData) { - const price = parseFloat(salesUnitData.actual_price || salesUnitData.price || 0); + const price = parsePrice(salesUnitData.actual_price || salesUnitData.price || 0); if (price > 0) { priceCache[cacheKey] = price; console.log('getSalesUnitPrice: from API', salesUnitId, price);