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:
2025-12-04 13:11:50 +03:00
parent 4de89fca43
commit 16234b0a1f
4 changed files with 122 additions and 79 deletions

View File

@@ -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() {

View File

@@ -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">
{

View File

@@ -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)
})