fix(pos): исправлены проблемы с ценами витринных комплектов
- Исправлена логика установки useSalePrice при загрузке данных комплекта - Исправлено сохранение sale_price при снятии чекбокса 'Установить свою цену' - Исправлено сохранение измененных цен товаров в составе комплекта (unit_price) - Добавлен блок предупреждения о неактуальных ценах с функцией пересчета - Улучшена логика агрегации товаров при сохранении комплекта
This commit is contained in:
@@ -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 = '<i class="bi bi-exclamation-triangle-fill text-white" style="font-size: 14px;"></i>';
|
||||
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 = '<i class="bi bi-pencil"></i>';
|
||||
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 = '<i class="bi bi-pencil-fill text-white" style="font-size: 14px;"></i>';
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Индикация блокировки
|
||||
|
||||
@@ -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
|
||||
});
|
||||
});
|
||||
|
||||
@@ -270,6 +270,20 @@
|
||||
<strong>Ценообразование</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<!-- Предупреждение о неактуальных ценах -->
|
||||
<div class="alert alert-warning mb-2" id="priceOutdatedWarning" style="display: none;">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<div>
|
||||
<i class="bi bi-exclamation-triangle me-2"></i>
|
||||
<strong>Цены товаров неактуальны</strong>
|
||||
<div class="small mt-1">Некоторые товары имеют устаревшие цены. Рекомендуется пересчитать.</div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-warning" id="recalculatePricesBtn">
|
||||
<i class="bi bi-arrow-clockwise"></i> Пересчитать
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Базовая цена -->
|
||||
<div class="mb-2">
|
||||
<small class="text-muted">Базовая цена (сумма компонентов):</small>
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user