FIX: Добавлен баланс кошелька клиента в модальное окно продажи POS
Проблема: - Баланс кошелька клиента не отображался в модальном окне при нажатии "ПРОДАТЬ" - Данные о балансе не передавались из backend в frontend Исправления: 1. pos/views.py: - Добавлен wallet_balance в selected_customer при загрузке из Redis - Добавлен wallet_balance в system_customer - Добавлен wallet_balance в API set_customer (Redis + response) - Используется json.dumps() для корректной сериализации данных клиента 2. customers/views.py: - Добавлен wallet_balance в API поиска клиентов (api_search_customers) - Добавлен wallet_balance в API создания клиента (api_create_customer) 3. pos/static/pos/js/terminal.js: - Обновлена функция selectCustomer() для получения walletBalance - Обновлены все вызовы selectCustomer() для передачи баланса - selectedCustomer теперь содержит wallet_balance 4. pos/templates/pos/terminal.html: - Используются готовые JSON-строки из backend (system_customer_json, selected_customer_json) - Исправлена проблема с локализацией чисел в JSON Результат: Баланс кошелька клиента теперь корректно отображается в модальном окне продажи 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -86,6 +86,7 @@ function formatMoney(v) {
|
||||
* - Кнопку "Выбрать клиента" в корзине (показывает имя клиента)
|
||||
* - Кнопку "Выбрать клиента" в модалке продажи (показывает имя клиента)
|
||||
* - Видимость кнопок сброса в обоих местах (показываем только для не-системного клиента)
|
||||
* - Ссылку на анкету клиента (показываем только для не-системного клиента)
|
||||
*/
|
||||
function updateCustomerDisplay() {
|
||||
// Обновляем текст кнопки в корзине
|
||||
@@ -110,17 +111,30 @@ function updateCustomerDisplay() {
|
||||
resetBtn.style.display = isSystemCustomer ? 'none' : 'block';
|
||||
}
|
||||
});
|
||||
|
||||
// Обновляем ссылку на анкету клиента
|
||||
const profileLink = document.getElementById('customerProfileLink');
|
||||
if (profileLink) {
|
||||
if (isSystemCustomer) {
|
||||
profileLink.style.display = 'none';
|
||||
} else {
|
||||
profileLink.href = `/customers/${selectedCustomer.id}/`;
|
||||
profileLink.style.display = 'block';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Устанавливает нового клиента и сохраняет в Redis
|
||||
* @param {number} customerId - ID клиента
|
||||
* @param {string} customerName - Имя клиента
|
||||
* @param {number} walletBalance - Баланс кошелька клиента (опционально)
|
||||
*/
|
||||
function selectCustomer(customerId, customerName) {
|
||||
function selectCustomer(customerId, customerName, walletBalance = 0) {
|
||||
selectedCustomer = {
|
||||
id: customerId,
|
||||
name: customerName
|
||||
name: customerName,
|
||||
wallet_balance: walletBalance
|
||||
};
|
||||
updateCustomerDisplay();
|
||||
|
||||
@@ -136,6 +150,9 @@ function selectCustomer(customerId, customerName) {
|
||||
.then(data => {
|
||||
if (!data.success) {
|
||||
console.error('Ошибка сохранения клиента:', data.error);
|
||||
} else {
|
||||
// Обновляем баланс из ответа сервера
|
||||
selectedCustomer.wallet_balance = data.wallet_balance || 0;
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
@@ -188,8 +205,8 @@ function initCustomerSelect2() {
|
||||
return;
|
||||
}
|
||||
|
||||
// Выбираем клиента
|
||||
selectCustomer(parseInt(data.id), data.name);
|
||||
// Выбираем клиента с балансом
|
||||
selectCustomer(parseInt(data.id), data.name, data.wallet_balance || 0);
|
||||
|
||||
// Закрываем модалку
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('selectCustomerModal'));
|
||||
@@ -297,8 +314,8 @@ async function createNewCustomer() {
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Выбираем созданного клиента
|
||||
selectCustomer(data.id, data.name);
|
||||
// Выбираем созданного клиента с балансом
|
||||
selectCustomer(data.id, data.name, data.wallet_balance || 0);
|
||||
|
||||
// Закрываем модалку
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('createCustomerModal'));
|
||||
@@ -516,33 +533,46 @@ function renderProducts() {
|
||||
stock.style.color = '#856404';
|
||||
stock.style.fontWeight = 'bold';
|
||||
} else if (item.type === 'product' && item.available_qty !== undefined && item.reserved_qty !== undefined) {
|
||||
// Для обычных товаров показываем остатки: FREE(-RESERVED)
|
||||
// FREE = доступно для продажи (available - reserved)
|
||||
// Для обычных товаров показываем остатки: FREE(-RESERVED-IN_CART)
|
||||
// FREE = доступно для продажи (available - reserved - в корзине)
|
||||
const available = parseFloat(item.available_qty) || 0;
|
||||
const reserved = parseFloat(item.reserved_qty) || 0;
|
||||
const free = available - reserved;
|
||||
|
||||
|
||||
// Вычитаем количество из корзины для визуализации
|
||||
const cartKey = `product-${item.id}`;
|
||||
const inCart = cart.has(cartKey) ? cart.get(cartKey).qty : 0;
|
||||
|
||||
const free = available - reserved - inCart;
|
||||
|
||||
// Создаём элементы для стилизации разных размеров
|
||||
const freeSpan = document.createElement('span');
|
||||
freeSpan.textContent = free;
|
||||
freeSpan.style.fontSize = '1.1em';
|
||||
freeSpan.style.fontWeight = 'bold';
|
||||
freeSpan.style.fontStyle = 'normal';
|
||||
|
||||
// Отображаем резерв только если он есть
|
||||
|
||||
// Отображаем резерв и корзину если они есть
|
||||
const suffixParts = [];
|
||||
if (reserved > 0) {
|
||||
const reservedSpan = document.createElement('span');
|
||||
reservedSpan.textContent = `(−${reserved})`;
|
||||
reservedSpan.style.fontSize = '0.85em';
|
||||
reservedSpan.style.marginLeft = '3px';
|
||||
reservedSpan.style.fontStyle = 'normal';
|
||||
|
||||
suffixParts.push(`−${reserved}`);
|
||||
}
|
||||
if (inCart > 0) {
|
||||
suffixParts.push(`−${inCart}🛒`);
|
||||
}
|
||||
|
||||
if (suffixParts.length > 0) {
|
||||
const suffixSpan = document.createElement('span');
|
||||
suffixSpan.textContent = `(${suffixParts.join(' ')})`;
|
||||
suffixSpan.style.fontSize = '0.85em';
|
||||
suffixSpan.style.marginLeft = '3px';
|
||||
suffixSpan.style.fontStyle = 'normal';
|
||||
|
||||
stock.appendChild(freeSpan);
|
||||
stock.appendChild(reservedSpan);
|
||||
stock.appendChild(suffixSpan);
|
||||
} else {
|
||||
stock.appendChild(freeSpan);
|
||||
}
|
||||
|
||||
|
||||
// Цветовая индикация: красный если свободных остатков нет или отрицательные
|
||||
if (free <= 0) {
|
||||
stock.style.color = '#dc3545'; // Красный
|
||||
@@ -619,13 +649,13 @@ async function loadItems(append = false) {
|
||||
} else {
|
||||
ITEMS = data.items;
|
||||
}
|
||||
|
||||
|
||||
hasMoreItems = data.has_more;
|
||||
|
||||
|
||||
if (data.has_more) {
|
||||
currentPage = data.next_page;
|
||||
}
|
||||
|
||||
|
||||
renderProducts();
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -721,6 +751,11 @@ async function addToCart(item) {
|
||||
renderCart();
|
||||
saveCartToRedis(); // Сохраняем в Redis
|
||||
|
||||
// Перерисовываем товары для обновления визуального остатка
|
||||
if (!isShowcaseView && item.type === 'product') {
|
||||
renderProducts();
|
||||
}
|
||||
|
||||
// Автоматический фокус на поле количества (только для обычных товаров)
|
||||
if (item.type !== 'showcase_kit') {
|
||||
setTimeout(() => {
|
||||
@@ -735,17 +770,36 @@ async function addToCart(item) {
|
||||
}
|
||||
}
|
||||
|
||||
// Вспомогательная функция для обновления количества товара в корзине
|
||||
async function updateCartItemQty(cartKey, newQty) {
|
||||
const item = cart.get(cartKey);
|
||||
if (!item) return;
|
||||
|
||||
if (newQty <= 0) {
|
||||
await removeFromCart(cartKey);
|
||||
} else {
|
||||
item.qty = newQty;
|
||||
renderCart();
|
||||
saveCartToRedis();
|
||||
|
||||
// Перерисовываем товары для обновления визуального остатка
|
||||
if (!isShowcaseView && item.type === 'product') {
|
||||
renderProducts();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function renderCart() {
|
||||
const list = document.getElementById('cartList');
|
||||
list.innerHTML = '';
|
||||
let total = 0;
|
||||
|
||||
|
||||
if (cart.size === 0) {
|
||||
list.innerHTML = '<p class="text-muted text-center py-4 small">Корзина пуста</p>';
|
||||
document.getElementById('cartTotal').textContent = '0.00';
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
cart.forEach((item, cartKey) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'cart-item mb-2';
|
||||
@@ -799,16 +853,10 @@ function renderCart() {
|
||||
const minusBtn = document.createElement('button');
|
||||
minusBtn.className = 'btn btn-outline-secondary btn-sm';
|
||||
minusBtn.innerHTML = '<i class="bi bi-dash-circle" style="font-size: 1.2em;"></i>';
|
||||
minusBtn.onclick = (e) => {
|
||||
minusBtn.onclick = async (e) => {
|
||||
e.preventDefault();
|
||||
const currentQty = cart.get(cartKey).qty;
|
||||
if (currentQty <= 1) {
|
||||
removeFromCart(cartKey);
|
||||
} else {
|
||||
cart.get(cartKey).qty = currentQty - 1;
|
||||
renderCart();
|
||||
saveCartToRedis();
|
||||
}
|
||||
await updateCartItemQty(cartKey, currentQty - 1);
|
||||
};
|
||||
|
||||
// Поле ввода количества
|
||||
@@ -820,26 +868,19 @@ function renderCart() {
|
||||
qtyInput.style.padding = '0.375rem 0.25rem';
|
||||
qtyInput.value = item.qty;
|
||||
qtyInput.min = 1;
|
||||
qtyInput.onchange = (e) => {
|
||||
qtyInput.onchange = async (e) => {
|
||||
const newQty = parseInt(e.target.value) || 1;
|
||||
if (newQty <= 0) {
|
||||
removeFromCart(cartKey);
|
||||
} else {
|
||||
cart.get(cartKey).qty = newQty;
|
||||
renderCart();
|
||||
saveCartToRedis(); // Сохраняем в Redis при изменении количества
|
||||
}
|
||||
await updateCartItemQty(cartKey, newQty);
|
||||
};
|
||||
|
||||
// Кнопка плюс
|
||||
const plusBtn = document.createElement('button');
|
||||
plusBtn.className = 'btn btn-outline-secondary btn-sm';
|
||||
plusBtn.innerHTML = '<i class="bi bi-plus-circle" style="font-size: 1.2em;"></i>';
|
||||
plusBtn.onclick = (e) => {
|
||||
plusBtn.onclick = async (e) => {
|
||||
e.preventDefault();
|
||||
cart.get(cartKey).qty += 1;
|
||||
renderCart();
|
||||
saveCartToRedis();
|
||||
const currentQty = cart.get(cartKey).qty;
|
||||
await updateCartItemQty(cartKey, currentQty + 1);
|
||||
};
|
||||
|
||||
// Собираем контейнер
|
||||
@@ -908,6 +949,11 @@ async function removeFromCart(cartKey) {
|
||||
cart.delete(cartKey);
|
||||
renderCart();
|
||||
saveCartToRedis(); // Сохраняем в Redis
|
||||
|
||||
// Перерисовываем товары для обновления визуального остатка
|
||||
if (!isShowcaseView && item && item.type === 'product') {
|
||||
renderProducts();
|
||||
}
|
||||
}
|
||||
|
||||
function clearCart() {
|
||||
|
||||
@@ -69,6 +69,9 @@
|
||||
<span id="customerSelectBtnText" class="fw-semibold">Выбрать</span>
|
||||
</div>
|
||||
</button>
|
||||
<a href="#" id="customerProfileLink" class="btn btn-sm btn-outline-secondary" title="Открыть анкету клиента" target="_blank" style="display: none;">
|
||||
<i class="bi bi-box-arrow-up-right"></i>
|
||||
</a>
|
||||
<button class="btn btn-sm btn-outline-danger" id="resetCustomerBtn" title="Сбросить на системного клиента" style="display: none;">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
@@ -484,18 +487,8 @@
|
||||
<script id="categoriesData" type="application/json">{{ categories_json|safe }}</script>
|
||||
<script id="itemsData" type="application/json">{{ items_json|safe }}</script>
|
||||
<script id="showcaseKitsData" type="application/json">{{ showcase_kits_json|safe }}</script>
|
||||
<script id="systemCustomerData" type="application/json">
|
||||
{
|
||||
"id": {{ system_customer.id }},
|
||||
"name": "{{ system_customer.name|escapejs }}"
|
||||
}
|
||||
</script>
|
||||
<script id="selectedCustomerData" type="application/json">
|
||||
{
|
||||
"id": {{ selected_customer.id }},
|
||||
"name": "{{ selected_customer.name|escapejs }}"
|
||||
}
|
||||
</script>
|
||||
<script id="systemCustomerData" type="application/json">{{ system_customer_json|safe }}</script>
|
||||
<script id="selectedCustomerData" type="application/json">{{ selected_customer_json|safe }}</script>
|
||||
<script id="cartData" type="application/json">{{ cart_data|safe }}</script>
|
||||
<script id="currentWarehouseData" type="application/json">
|
||||
{
|
||||
|
||||
@@ -181,14 +181,16 @@ def pos_terminal(request):
|
||||
'showcase_kits_json': json.dumps([]),
|
||||
'current_warehouse': None,
|
||||
'warehouses': [],
|
||||
'system_customer': {
|
||||
'system_customer_json': json.dumps({
|
||||
'id': system_customer.id,
|
||||
'name': system_customer.name
|
||||
},
|
||||
'selected_customer': {
|
||||
'name': system_customer.name,
|
||||
'wallet_balance': float(system_customer.wallet_balance)
|
||||
}),
|
||||
'selected_customer_json': json.dumps({
|
||||
'id': system_customer.id,
|
||||
'name': system_customer.name
|
||||
},
|
||||
'name': system_customer.name,
|
||||
'wallet_balance': float(system_customer.wallet_balance)
|
||||
}),
|
||||
'cart_data': json.dumps({}),
|
||||
'title': 'POS Terminal',
|
||||
}
|
||||
@@ -208,7 +210,8 @@ def pos_terminal(request):
|
||||
customer = Customer.objects.get(id=cached_customer_data['customer_id'])
|
||||
selected_customer = {
|
||||
'id': customer.id,
|
||||
'name': customer.name
|
||||
'name': customer.name,
|
||||
'wallet_balance': float(customer.wallet_balance)
|
||||
}
|
||||
except Customer.DoesNotExist:
|
||||
# Клиент был удален - очищаем кэш
|
||||
@@ -218,7 +221,8 @@ def pos_terminal(request):
|
||||
if not selected_customer:
|
||||
selected_customer = {
|
||||
'id': system_customer.id,
|
||||
'name': system_customer.name
|
||||
'name': system_customer.name,
|
||||
'wallet_balance': float(system_customer.wallet_balance)
|
||||
}
|
||||
|
||||
# Пытаемся получить сохраненную корзину из Redis
|
||||
@@ -280,11 +284,12 @@ def pos_terminal(request):
|
||||
'name': current_warehouse.name
|
||||
},
|
||||
'warehouses': warehouses_list,
|
||||
'system_customer': {
|
||||
'system_customer_json': json.dumps({
|
||||
'id': system_customer.id,
|
||||
'name': system_customer.name
|
||||
},
|
||||
'selected_customer': selected_customer, # Текущий выбранный клиент (из Redis или системный)
|
||||
'name': system_customer.name,
|
||||
'wallet_balance': float(system_customer.wallet_balance)
|
||||
}),
|
||||
'selected_customer_json': json.dumps(selected_customer), # Текущий выбранный клиент (из Redis или системный)
|
||||
'cart_data': json.dumps(cart_data), # Сохраненная корзина из Redis
|
||||
'title': 'POS Terminal',
|
||||
}
|
||||
@@ -355,14 +360,16 @@ def set_customer(request, customer_id):
|
||||
redis_key = f'pos:customer:{request.user.id}:{current_warehouse.id}'
|
||||
customer_data = {
|
||||
'customer_id': customer.id,
|
||||
'customer_name': customer.name
|
||||
'customer_name': customer.name,
|
||||
'wallet_balance': float(customer.wallet_balance)
|
||||
}
|
||||
cache.set(redis_key, customer_data, timeout=7200) # 2 часа
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'customer_id': customer.id,
|
||||
'customer_name': customer.name
|
||||
'customer_name': customer.name,
|
||||
'wallet_balance': float(customer.wallet_balance)
|
||||
})
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user