diff --git a/myproject/pos/static/pos/js/products.js b/myproject/pos/static/pos/js/products.js index ed127a7..6c67d77 100644 --- a/myproject/pos/static/pos/js/products.js +++ b/myproject/pos/static/pos/js/products.js @@ -519,14 +519,74 @@ export class ProductManager { _renderShowcaseKitBadges(card, item, cart) { // Кнопка редактирования (только если не заблокирован другим) if (!item.is_locked || item.locked_by_me) { + // Индикатор неактуальной цены (показываем первым, если есть) + if (item.price_outdated) { + const outdatedBadge = document.createElement('button'); + outdatedBadge.className = 'btn btn-sm p-0'; + outdatedBadge.style.position = 'absolute'; + outdatedBadge.style.top = '8px'; + outdatedBadge.style.right = '45px'; + outdatedBadge.style.zIndex = '10'; + outdatedBadge.style.width = '28px'; + outdatedBadge.style.height = '28px'; + outdatedBadge.style.borderRadius = '50%'; + outdatedBadge.style.display = 'flex'; + outdatedBadge.style.alignItems = 'center'; + outdatedBadge.style.justifyContent = 'center'; + outdatedBadge.style.backgroundColor = '#ff6b6b'; + outdatedBadge.style.border = '2px solid #fff'; + outdatedBadge.style.boxShadow = '0 2px 4px rgba(0,0,0,0.2)'; + outdatedBadge.style.cursor = 'pointer'; + outdatedBadge.style.transition = 'all 0.2s ease'; + outdatedBadge.title = 'Цена неактуальна - требуется обновление'; + outdatedBadge.innerHTML = ''; + outdatedBadge.onmouseenter = function() { + this.style.transform = 'scale(1.1)'; + this.style.boxShadow = '0 3px 6px rgba(0,0,0,0.3)'; + }; + outdatedBadge.onmouseleave = function() { + this.style.transform = 'scale(1)'; + this.style.boxShadow = '0 2px 4px rgba(0,0,0,0.2)'; + }; + outdatedBadge.onclick = (e) => { + e.stopPropagation(); + if (window.showcaseManager) { + window.showcaseManager.openEditModal(item.id); + } + }; + card.appendChild(outdatedBadge); + } + + // Кнопка редактирования (карандаш) const editBtn = document.createElement('button'); - editBtn.className = 'btn btn-sm btn-outline-primary'; + editBtn.className = 'btn btn-sm p-0'; editBtn.style.position = 'absolute'; - editBtn.style.top = '5px'; - editBtn.style.right = '5px'; + editBtn.style.top = '8px'; + editBtn.style.right = '8px'; editBtn.style.zIndex = '10'; - editBtn.innerHTML = ''; + editBtn.style.width = '32px'; + editBtn.style.height = '32px'; + editBtn.style.borderRadius = '6px'; + editBtn.style.display = 'flex'; + editBtn.style.alignItems = 'center'; + editBtn.style.justifyContent = 'center'; + editBtn.style.backgroundColor = '#4dabf7'; + editBtn.style.border = '2px solid #fff'; + editBtn.style.boxShadow = '0 2px 4px rgba(0,0,0,0.2)'; + editBtn.style.cursor = 'pointer'; + editBtn.style.transition = 'all 0.2s ease'; editBtn.title = 'Редактировать комплект'; + editBtn.innerHTML = ''; + editBtn.onmouseenter = function() { + this.style.backgroundColor = '#339af0'; + this.style.transform = 'scale(1.05)'; + this.style.boxShadow = '0 3px 6px rgba(0,0,0,0.3)'; + }; + editBtn.onmouseleave = function() { + this.style.backgroundColor = '#4dabf7'; + this.style.transform = 'scale(1)'; + this.style.boxShadow = '0 2px 4px rgba(0,0,0,0.2)'; + }; editBtn.onclick = (e) => { e.stopPropagation(); if (window.showcaseManager) { @@ -534,28 +594,6 @@ export class ProductManager { } }; card.appendChild(editBtn); - - // Индикатор неактуальной цены (красный кружок) - if (item.price_outdated) { - const outdatedBadge = document.createElement('div'); - outdatedBadge.className = 'badge bg-danger'; - outdatedBadge.style.position = 'absolute'; - outdatedBadge.style.top = '5px'; - outdatedBadge.style.right = '45px'; - outdatedBadge.style.zIndex = '10'; - outdatedBadge.style.width = '18px'; - outdatedBadge.style.height = '18px'; - outdatedBadge.style.padding = '0'; - outdatedBadge.style.borderRadius = '50%'; - outdatedBadge.style.display = 'flex'; - outdatedBadge.style.alignItems = 'center'; - outdatedBadge.style.justifyContent = 'center'; - outdatedBadge.style.fontSize = '10px'; - outdatedBadge.style.minWidth = '18px'; - outdatedBadge.title = 'Цена неактуальна'; - outdatedBadge.innerHTML = '!'; - card.appendChild(outdatedBadge); - } } // Индикация блокировки diff --git a/myproject/pos/static/pos/js/showcase.js b/myproject/pos/static/pos/js/showcase.js index 8284ed1..dbb562f 100644 --- a/myproject/pos/static/pos/js/showcase.js +++ b/myproject/pos/static/pos/js/showcase.js @@ -85,6 +85,9 @@ export class ShowcaseManager { document.getElementById('priceAdjustmentValue')?.addEventListener('input', () => this.updatePriceCalculation()); document.getElementById('useSalePrice')?.addEventListener('change', () => this.updatePriceCalculation()); document.getElementById('salePrice')?.addEventListener('input', () => this.updatePriceCalculation()); + + // Кнопка пересчета цен + document.getElementById('recalculatePricesBtn')?.addEventListener('click', () => this.recalculatePrices()); } /** @@ -157,26 +160,44 @@ export class ShowcaseManager { // Заполняем временную корзину this.tempCart.clear(); + let hasOutdatedPrices = false; if (kit.items) { kit.items.forEach(item => { const key = `product-${item.product_id}-${item.sales_unit_id || 'base'}`; + const actualPrice = item.actual_catalog_price ? parseFloat(item.actual_catalog_price) : null; + const currentPrice = parseFloat(item.price) || 0; + const isOutdated = item.price_outdated || (actualPrice !== null && Math.abs(currentPrice - actualPrice) > 0.01); + + if (isOutdated) { + hasOutdatedPrices = true; + } + this.tempCart.set(key, { id: item.product_id, name: item.name || item.product_name, // Сервер отдаёт name - price: parseFloat(item.price) || 0, + price: currentPrice, qty: parseFloat(item.qty || item.quantity) || 1, // Сервер отдаёт qty type: 'product', sales_unit_id: item.sales_unit_id, - unit_name: item.unit_name + unit_name: item.unit_name, + actual_catalog_price: actualPrice, // Сохраняем актуальную цену для пересчета + price_outdated: isOutdated }); }); } + + // Показываем блок предупреждения о неактуальных ценах + this.updatePriceOutdatedWarning(hasOutdatedPrices); // Заполняем цены document.getElementById('priceAdjustmentType').value = kit.price_adjustment_type || 'none'; document.getElementById('priceAdjustmentValue').value = kit.price_adjustment_value || 0; - document.getElementById('useSalePrice').checked = kit.use_sale_price || false; - document.getElementById('salePrice').value = kit.sale_price || ''; + + // Если sale_price установлен, автоматически включаем useSalePrice + const salePriceValue = kit.sale_price || ''; + const hasSalePrice = salePriceValue && parseFloat(salePriceValue) > 0; + document.getElementById('useSalePrice').checked = hasSalePrice; + document.getElementById('salePrice').value = salePriceValue; this.renderTempKitItems(); this.updatePriceCalculation(); @@ -363,6 +384,15 @@ export class ShowcaseManager { // Обновляем базовую цену document.getElementById('tempKitBasePrice').textContent = formatMoney(totalBasePrice) + ' руб.'; + + // Проверяем наличие неактуальных цен и обновляем предупреждение + let hasOutdatedPrices = false; + this.tempCart.forEach((item) => { + if (item.price_outdated || (item.actual_catalog_price !== null && item.actual_catalog_price !== undefined && Math.abs(item.price - item.actual_catalog_price) > 0.01)) { + hasOutdatedPrices = true; + } + }); + this.updatePriceOutdatedWarning(hasOutdatedPrices); } /** @@ -416,6 +446,50 @@ export class ShowcaseManager { document.getElementById('tempKitFinalPrice').textContent = formatMoney(finalPrice); } + /** + * Обновляет отображение предупреждения о неактуальных ценах + * @param {boolean} hasOutdated - Есть ли неактуальные цены + */ + updatePriceOutdatedWarning(hasOutdated) { + const warningBlock = document.getElementById('priceOutdatedWarning'); + if (warningBlock) { + warningBlock.style.display = hasOutdated ? 'block' : 'none'; + } + } + + /** + * Пересчитывает цены товаров на актуальные из каталога + */ + recalculatePrices() { + let updated = false; + + this.tempCart.forEach((item, key) => { + if (item.actual_catalog_price !== null && item.actual_catalog_price !== undefined) { + const oldPrice = item.price; + const newPrice = item.actual_catalog_price; + + if (Math.abs(oldPrice - newPrice) > 0.01) { + item.price = newPrice; + item.price_outdated = false; + updated = true; + } + } + }); + + if (updated) { + // Перерисовываем список товаров и пересчитываем цены + this.renderTempKitItems(); + this.updatePriceCalculation(); + + // Скрываем предупреждение + this.updatePriceOutdatedWarning(false); + + showToast('success', 'Цены обновлены на актуальные из каталога'); + } else { + showToast('info', 'Все цены уже актуальны'); + } + } + /** * Обрабатывает загрузку фото */ @@ -462,6 +536,8 @@ export class ShowcaseManager { document.getElementById('useSalePrice').checked = false; document.getElementById('salePrice').value = ''; this.removePhoto(); + // Скрываем предупреждение о неактуальных ценах + this.updatePriceOutdatedWarning(false); } /** @@ -537,8 +613,9 @@ export class ShowcaseManager { formData.append('showcase_created_at', data.createdAt || ''); formData.append('price_adjustment_type', data.adjustmentType); formData.append('price_adjustment_value', data.adjustmentValue); - formData.append('use_sale_price', data.useSalePrice); - formData.append('sale_price', data.salePrice || 0); + formData.append('use_sale_price', data.useSalePrice ? '1' : '0'); + // Если useSalePrice выключен, отправляем пустую строку для явной очистки sale_price на сервере + formData.append('sale_price', data.useSalePrice ? (data.salePrice || '') : ''); if (!this.isEditMode) { formData.append('quantity', data.quantity); @@ -550,7 +627,7 @@ export class ShowcaseManager { items.push({ product_id: item.id, quantity: item.qty, - price: item.price, + unit_price: item.price, // Используем unit_price для сохранения измененной цены товара sales_unit_id: item.sales_unit_id || null }); }); diff --git a/myproject/pos/templates/pos/terminal.html b/myproject/pos/templates/pos/terminal.html index 1f9148b..9d7fd96 100644 --- a/myproject/pos/templates/pos/terminal.html +++ b/myproject/pos/templates/pos/terminal.html @@ -270,6 +270,20 @@ Ценообразование
+ + +
Базовая цена (сумма компонентов): diff --git a/myproject/pos/views.py b/myproject/pos/views.py index 3107d6d..5c5372a 100644 --- a/myproject/pos/views.py +++ b/myproject/pos/views.py @@ -139,13 +139,14 @@ def get_showcase_kits_for_pos(): status='available' ).values_list('id', flat=True)) - # Определяем актуальную цену + # Определяем актуальную цену продажи (sale_price имеет приоритет) price = item['product_kit__sale_price'] or item['product_kit__price'] - # Проверяем актуальность цены (сравниваем сохранённую цену с актуальной ценой товаров) + # Проверяем актуальность цены (сравниваем сохранённую цену продажи с актуальной ценой компонентов) actual_price = kit_actual_prices.get(kit_id, Decimal('0')) - base_price = item['product_kit__base_price'] - price_outdated = base_price and abs(float(base_price) - float(actual_price)) > 0.01 + # Сравниваем цену продажи с актуальной ценой компонентов + # Если разница больше 0.01, значит цена неактуальна + price_outdated = actual_price > 0 and abs(float(price) - float(actual_price)) > 0.01 showcase_kits.append({ 'id': kit_id, @@ -1030,9 +1031,15 @@ def get_product_kit_details(request, kit_id): 'qty': str(ki.quantity), 'price': str(item_price) } - # Для временных комплектов добавляем актуальную цену из каталога для сравнения - if kit.is_temporary and ki.unit_price is not None: + # Для временных комплектов всегда добавляем актуальную цену из каталога для сравнения + if kit.is_temporary: item_data['actual_catalog_price'] = str(ki.product.actual_price) + # Проверяем, неактуальна ли цена (если unit_price установлен и отличается от actual_price) + if ki.unit_price is not None: + price_diff = abs(float(ki.unit_price) - float(ki.product.actual_price)) + item_data['price_outdated'] = price_diff > 0.01 + else: + item_data['price_outdated'] = False items.append(item_data) # Фото (используем миниатюру для быстрой загрузки) @@ -1099,15 +1106,23 @@ def create_temp_kit_to_showcase(request): # Парсим items из JSON items = json.loads(items_json) + # Получаем флаг use_sale_price для явной очистки sale_price + use_sale_price = request.POST.get('use_sale_price', '0') == '1' + # Sale price (опционально) sale_price = None - if sale_price_str: + # Если use_sale_price = True, обрабатываем sale_price_str + # Если use_sale_price = False, явно устанавливаем sale_price = None + if use_sale_price and sale_price_str: try: sale_price = Decimal(str(sale_price_str)) if sale_price <= 0: sale_price = None except (ValueError, InvalidOperation): sale_price = None + else: + # Явно очищаем sale_price, если чекбокс выключен + sale_price = None # Showcase created at (опционально) showcase_created_at = None @@ -1174,6 +1189,9 @@ def create_temp_kit_to_showcase(request): if product_id in aggregated_items: aggregated_items[product_id]['quantity'] += quantity + # Если unit_price не был установлен ранее, но есть в текущем элементе, устанавливаем его + if aggregated_items[product_id]['unit_price'] is None and unit_price is not None: + aggregated_items[product_id]['unit_price'] = Decimal(str(unit_price)) else: aggregated_items[product_id] = { 'quantity': quantity, @@ -1333,14 +1351,22 @@ def update_product_kit(request, kit_id): items = json.loads(items_json) + # Получаем флаг use_sale_price для явной очистки sale_price + use_sale_price = request.POST.get('use_sale_price', '0') == '1' + sale_price = None - if sale_price_str: + # Если use_sale_price = True, обрабатываем sale_price_str + # Если use_sale_price = False, явно устанавливаем sale_price = None + if use_sale_price and sale_price_str: try: sale_price = Decimal(str(sale_price_str)) if sale_price <= 0: sale_price = None except (ValueError, InvalidOperation): sale_price = None + else: + # Явно очищаем sale_price, если чекбокс выключен + sale_price = None # Showcase created at (опционально) showcase_created_at = None @@ -1381,6 +1407,9 @@ def update_product_kit(request, kit_id): unit_price = item.get('unit_price') if product_id in aggregated_items: aggregated_items[product_id]['quantity'] += quantity + # Если unit_price не был установлен ранее, но есть в текущем элементе, устанавливаем его + if aggregated_items[product_id]['unit_price'] is None and unit_price is not None: + aggregated_items[product_id]['unit_price'] = Decimal(str(unit_price)) else: aggregated_items[product_id] = { 'quantity': quantity,