Add Redis-based persistence for POS cart and customer selection

Implemented Redis caching with 2-hour TTL for POS session data:

Backend changes:
- Added Redis cache configuration in settings.py
- Created save_cart() endpoint to persist cart state
- Added cart and customer loading from Redis in pos_terminal()
- Validates cart items (products/kits) still exist in DB
- Added REDIS_HOST, REDIS_PORT, REDIS_DB to .env

Frontend changes:
- Added saveCartToRedis() with 500ms debounce
- Cart auto-saves on add/remove/quantity change
- Added cart initialization from Redis on page load
- Enhanced customer button with two-line display and reset button
- Red X button appears only for non-system customers

Features:
- Cart persists across page reloads (2 hour TTL)
- Customer selection persists (2 hour TTL)
- Independent cart per user+warehouse combination
- Automatic cleanup of deleted items
- Debounced saves to reduce server load

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-20 09:55:03 +03:00
parent 685c06d94d
commit eac778b06d
5 changed files with 614 additions and 19 deletions

View File

@@ -363,6 +363,20 @@ TENANT_ADMIN_NAME = env('TENANT_ADMIN_NAME')
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
# ============================================
# CACHE CONFIGURATION (Redis)
# ============================================
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.redis.RedisCache',
'LOCATION': f'redis://{env("REDIS_HOST", default="localhost")}:{env("REDIS_PORT", default="6379")}/{env("REDIS_DB", default="0")}',
'KEY_PREFIX': 'myproject', # Префикс для всех ключей
'TIMEOUT': 300, # Таймаут по умолчанию (5 минут)
}
}
# ============================================ # ============================================
# CELERY CONFIGURATION # CELERY CONFIGURATION
# ============================================ # ============================================

View File

@@ -22,10 +22,297 @@ let editingKitId = null;
// Временная корзина для модального окна создания/редактирования комплекта // Временная корзина для модального окна создания/редактирования комплекта
const tempCart = new Map(); const tempCart = new Map();
// ===== СОХРАНЕНИЕ КОРЗИНЫ В REDIS =====
let saveCartTimeout = null;
/**
* Сохраняет корзину в Redis с debounce 500ms
*/
function saveCartToRedis() {
// Отменяем предыдущий таймер
if (saveCartTimeout) {
clearTimeout(saveCartTimeout);
}
// Устанавливаем новый таймер
saveCartTimeout = setTimeout(() => {
// Конвертируем Map в обычный объект
const cartObj = {};
cart.forEach((value, key) => {
cartObj[key] = value;
});
// Отправляем на сервер
fetch('/pos/api/save-cart/', {
method: 'POST',
headers: {
'X-CSRFToken': getCsrfToken(),
'Content-Type': 'application/json'
},
body: JSON.stringify({ cart: cartObj })
})
.then(response => response.json())
.then(data => {
if (!data.success) {
console.error('Ошибка сохранения корзины:', data.error);
}
})
.catch(error => {
console.error('Ошибка при сохранении корзины в Redis:', error);
});
}, 500); // Debounce 500ms
}
// ===== УПРАВЛЕНИЕ КЛИЕНТОМ =====
// Загружаем данные системного клиента
const SYSTEM_CUSTOMER = JSON.parse(document.getElementById('systemCustomerData').textContent);
// Текущий выбранный клиент (загружается из Redis или системный)
let selectedCustomer = JSON.parse(document.getElementById('selectedCustomerData').textContent);
function formatMoney(v) { function formatMoney(v) {
return (Number(v)).toFixed(2); return (Number(v)).toFixed(2);
} }
// ===== ФУНКЦИИ ДЛЯ РАБОТЫ С КЛИЕНТОМ =====
/**
* Обновляет отображение выбранного клиента в UI
* Обновляет:
* - Кнопку "Выбрать клиента" в корзине (показывает имя клиента)
* - Имя клиента в модалке продажи
* - Видимость кнопки сброса (показываем только для не-системного клиента)
*/
function updateCustomerDisplay() {
// Обновляем текст кнопки - всегда показываем имя клиента
const btnText = document.getElementById('customerSelectBtnText');
btnText.textContent = selectedCustomer.name;
// Обновляем имя клиента в модалке продажи
const checkoutCustomerName = document.getElementById('checkoutCustomerName');
if (checkoutCustomerName) {
checkoutCustomerName.textContent = selectedCustomer.name;
}
// Показываем/скрываем кнопку сброса
const resetBtn = document.getElementById('resetCustomerBtn');
if (resetBtn) {
// Показываем кнопку сброса только если выбран НЕ системный клиент
if (selectedCustomer.id !== SYSTEM_CUSTOMER.id) {
resetBtn.style.display = 'block';
} else {
resetBtn.style.display = 'none';
}
}
}
/**
* Устанавливает нового клиента и сохраняет в Redis
* @param {number} customerId - ID клиента
* @param {string} customerName - Имя клиента
*/
function selectCustomer(customerId, customerName) {
selectedCustomer = {
id: customerId,
name: customerName
};
updateCustomerDisplay();
// Сохраняем выбор в Redis через AJAX
fetch(`/pos/api/set-customer/${customerId}/`, {
method: 'POST',
headers: {
'X-CSRFToken': getCsrfToken(),
'Content-Type': 'application/json'
}
})
.then(response => response.json())
.then(data => {
if (!data.success) {
console.error('Ошибка сохранения клиента:', data.error);
}
})
.catch(error => {
console.error('Ошибка при сохранении клиента в Redis:', error);
});
}
/**
* Инициализация Select2 для поиска клиента
*/
function initCustomerSelect2() {
const $searchInput = $('#customerSearchInput');
$searchInput.select2({
theme: 'bootstrap-5',
dropdownParent: $('#selectCustomerModal'),
placeholder: 'Начните вводить имя, телефон или email (минимум 3 символа)',
minimumInputLength: 3,
allowClear: true,
ajax: {
url: '/customers/api/search/',
dataType: 'json',
delay: 300,
data: function(params) {
return {
q: params.term
};
},
processResults: function(data) {
return {
results: data.results
};
},
cache: true
},
templateResult: formatCustomerOption, // Форматирование результатов в выпадающем списке
templateSelection: formatCustomerSelection // Форматирование выбранного значения
});
// Обработка выбора клиента из списка
$searchInput.on('select2:select', function(e) {
const data = e.params.data;
// Проверяем это не опция "Создать нового клиента"
if (data.id === 'create_new') {
// Открываем модалку создания
const modal = bootstrap.Modal.getInstance(document.getElementById('selectCustomerModal'));
modal.hide();
openCreateCustomerModal(data.text);
return;
}
// Выбираем клиента
selectCustomer(parseInt(data.id), data.name);
// Закрываем модалку
const modal = bootstrap.Modal.getInstance(document.getElementById('selectCustomerModal'));
modal.hide();
// Очищаем Select2
$searchInput.val(null).trigger('change');
});
}
/**
* Форматирование опции клиента в выпадающем списке Select2
* Показывает: Имя, телефон, email в одну строку
*/
function formatCustomerOption(customer) {
if (customer.loading) {
return customer.text;
}
// Если это опция "Создать нового клиента"
if (customer.id === 'create_new') {
return $('<span><i class="bi bi-person-plus"></i> ' + customer.text + '</span>');
}
// Формируем текст в одну строку: Имя (жирным) + контакты (мелким)
const parts = [];
// Имя
const name = customer.name || customer.text;
parts.push('<span class="fw-bold">' + $('<div>').text(name).html() + '</span>');
// Телефон и Email
const contactInfo = [];
if (customer.phone) {
contactInfo.push($('<div>').text(customer.phone).html());
}
if (customer.email) {
contactInfo.push($('<div>').text(customer.email).html());
}
if (contactInfo.length > 0) {
parts.push('<span class="text-muted small"> (' + contactInfo.join(', ') + ')</span>');
}
return $('<span>' + parts.join('') + '</span>');
}
/**
* Форматирование выбранного клиента в поле Select2
* Показывает только имя
*/
function formatCustomerSelection(customer) {
return customer.name || customer.text;
}
/**
* Открывает модальное окно создания нового клиента
* @param {string} prefillName - Предзаполненное имя (из поиска)
*/
function openCreateCustomerModal(prefillName = '') {
const modal = new bootstrap.Modal(document.getElementById('createCustomerModal'));
// Очищаем форму
document.getElementById('newCustomerName').value = prefillName || '';
document.getElementById('newCustomerPhone').value = '';
document.getElementById('newCustomerEmail').value = '';
document.getElementById('createCustomerError').classList.add('d-none');
modal.show();
}
/**
* Создаёт нового клиента через API
*/
async function createNewCustomer() {
const name = document.getElementById('newCustomerName').value.trim();
const phone = document.getElementById('newCustomerPhone').value.trim();
const email = document.getElementById('newCustomerEmail').value.trim();
const errorBlock = document.getElementById('createCustomerError');
// Валидация
if (!name) {
errorBlock.textContent = 'Укажите имя клиента';
errorBlock.classList.remove('d-none');
return;
}
// Скрываем ошибку
errorBlock.classList.add('d-none');
try {
const response = await fetch('/customers/api/create/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCsrfToken()
},
body: JSON.stringify({
name: name,
phone: phone || null,
email: email || null
})
});
const data = await response.json();
if (data.success) {
// Выбираем созданного клиента
selectCustomer(data.id, data.name);
// Закрываем модалку
const modal = bootstrap.Modal.getInstance(document.getElementById('createCustomerModal'));
modal.hide();
// Показываем уведомление
alert(`Клиент "${data.name}" успешно создан!`);
} else {
// Показываем ошибку
errorBlock.textContent = data.error || 'Ошибка при создании клиента';
errorBlock.classList.remove('d-none');
}
} catch (error) {
console.error('Error creating customer:', error);
errorBlock.textContent = 'Ошибка сети при создании клиента';
errorBlock.classList.remove('d-none');
}
}
function renderCategories() { function renderCategories() {
const grid = document.getElementById('categoryGrid'); const grid = document.getElementById('categoryGrid');
grid.innerHTML = ''; grid.innerHTML = '';
@@ -342,6 +629,7 @@ function addToCart(item) {
} }
renderCart(); renderCart();
saveCartToRedis(); // Сохраняем в Redis
// Автоматический фокус на поле количества // Автоматический фокус на поле количества
setTimeout(() => { setTimeout(() => {
@@ -403,6 +691,7 @@ function renderCart() {
} else { } else {
cart.get(cartKey).qty = newQty; cart.get(cartKey).qty = newQty;
renderCart(); renderCart();
saveCartToRedis(); // Сохраняем в Redis при изменении количества
} }
}; };
@@ -434,11 +723,13 @@ function renderCart() {
function removeFromCart(cartKey) { function removeFromCart(cartKey) {
cart.delete(cartKey); cart.delete(cartKey);
renderCart(); renderCart();
saveCartToRedis(); // Сохраняем в Redis
} }
function clearCart() { function clearCart() {
cart.clear(); cart.clear();
renderCart(); renderCart();
saveCartToRedis(); // Сохраняем пустую корзину в Redis
} }
document.getElementById('clearCart').onclick = clearCart; document.getElementById('clearCart').onclick = clearCart;
@@ -974,6 +1265,9 @@ function renderCheckoutModal() {
total += item.qty * item.price; total += item.qty * item.price;
}); });
// Обновляем информацию о клиенте
updateCustomerDisplay();
// Обновляем базовую цену и пересчитываем // Обновляем базовую цену и пересчитываем
updateCheckoutPricing(total); updateCheckoutPricing(total);
} }
@@ -1094,9 +1388,57 @@ document.getElementById('scheduleLater').onclick = async () => {
alert('Функционал будет подключен позже: создание заказа на доставку/самовывоз.'); alert('Функционал будет подключен позже: создание заказа на доставку/самовывоз.');
}; };
// Customer selection // ===== ОБРАБОТЧИКИ ДЛЯ РАБОТЫ С КЛИЕНТОМ =====
// Кнопка "Выбрать клиента" в корзине
document.getElementById('customerSelectBtn').addEventListener('click', () => { document.getElementById('customerSelectBtn').addEventListener('click', () => {
alert('Функция выбора клиента будет реализована позже'); const modal = new bootstrap.Modal(document.getElementById('selectCustomerModal'));
modal.show();
});
// Кнопка сброса клиента на системного
document.getElementById('resetCustomerBtn').addEventListener('click', () => {
selectCustomer(SYSTEM_CUSTOMER.id, SYSTEM_CUSTOMER.name);
});
// Кнопка "Создать нового клиента" в модалке выбора
document.getElementById('createNewCustomerBtn').addEventListener('click', () => {
// Закрываем модалку выбора
const selectModal = bootstrap.Modal.getInstance(document.getElementById('selectCustomerModal'));
selectModal.hide();
// Открываем модалку создания
openCreateCustomerModal();
});
// Кнопка "Выбрать системного клиента"
document.getElementById('selectSystemCustomerBtn').addEventListener('click', () => {
selectCustomer(SYSTEM_CUSTOMER.id, SYSTEM_CUSTOMER.name);
// Закрываем модалку
const modal = bootstrap.Modal.getInstance(document.getElementById('selectCustomerModal'));
modal.hide();
});
// Кнопка подтверждения создания клиента
document.getElementById('confirmCreateCustomerBtn').addEventListener('click', () => {
createNewCustomer();
});
// Инициализация Select2 при загрузке страницы
document.addEventListener('DOMContentLoaded', () => {
initCustomerSelect2();
updateCustomerDisplay(); // Обновляем UI с системным клиентом
// Восстанавливаем корзину из Redis (если есть сохраненные данные)
const savedCartData = JSON.parse(document.getElementById('cartData').textContent);
if (savedCartData && Object.keys(savedCartData).length > 0) {
// Конвертируем обычный объект обратно в Map
Object.entries(savedCartData).forEach(([key, value]) => {
cart.set(key, value);
});
renderCart(); // Отрисовываем восстановленную корзину
}
}); });
// Смена склада // Смена склада

View File

@@ -56,9 +56,18 @@
<div class="card mb-2 flex-grow-1" style="min-height: 0;"> <div class="card mb-2 flex-grow-1" style="min-height: 0;">
<div class="card-header bg-white d-flex justify-content-between align-items-center"> <div class="card-header bg-white d-flex justify-content-between align-items-center">
<h6 class="mb-0">Корзина</h6> <h6 class="mb-0">Корзина</h6>
<button class="btn btn-outline-primary btn-sm" id="customerSelectBtn"> <div class="d-flex gap-1 align-items-center">
<i class="bi bi-person"></i> Выбрать клиента <button class="btn btn-outline-primary btn-sm d-flex align-items-center" id="customerSelectBtn">
</button> <i class="bi bi-person me-1"></i>
<div class="d-flex flex-column align-items-start lh-1">
<small class="text-muted" style="font-size: 0.65rem;">Клиент</small>
<span id="customerSelectBtnText" class="fw-semibold">Выбрать</span>
</div>
</button>
<button class="btn btn-sm btn-outline-danger" id="resetCustomerBtn" title="Сбросить на системного клиента" style="display: none;">
<i class="bi bi-x-lg"></i>
</button>
</div>
</div> </div>
<div class="card-body d-flex flex-column" style="min-height: 0;"> <div class="card-body d-flex flex-column" style="min-height: 0;">
<div id="cartList" class="flex-grow-1" style="overflow-y: auto;"></div> <div id="cartList" class="flex-grow-1" style="overflow-y: auto;"></div>
@@ -266,9 +275,17 @@
<div class="row"> <div class="row">
<!-- Левая колонка: состав заказа --> <!-- Левая колонка: состав заказа -->
<div class="col-md-7"> <div class="col-md-7">
<!-- Информация о клиенте -->
<div class="mb-3">
<strong>Клиент</strong>
<div class="border rounded p-2 mt-2 bg-light">
<div class="fw-bold" id="checkoutCustomerName"></div>
</div>
</div>
<div class="mb-3"> <div class="mb-3">
<strong>Состав заказа</strong> <strong>Состав заказа</strong>
<div class="border rounded p-3 mt-2" id="checkoutItems" style="max-height: 280px; overflow-y: auto; background: #f8f9fa;"> <div class="border rounded p-3 mt-2" id="checkoutItems" style="max-height: 240px; overflow-y: auto; background: #f8f9fa;">
<!-- Заполняется из JS --> <!-- Заполняется из JS -->
</div> </div>
</div> </div>
@@ -394,6 +411,77 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Модалка: Выбор клиента -->
<div class="modal fade" id="selectCustomerModal" tabindex="-1" aria-labelledby="selectCustomerModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="selectCustomerModalLabel">
<i class="bi bi-person-search"></i> Выбор клиента
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="customerSearchInput" class="form-label">Поиск клиента</label>
<input type="text" class="form-select" id="customerSearchInput"
placeholder="Начните вводить имя, телефон или email (минимум 3 символа)">
</div>
<div class="d-grid gap-2">
<button type="button" class="btn btn-outline-success" id="createNewCustomerBtn">
<i class="bi bi-person-plus"></i> Создать нового клиента
</button>
<button type="button" class="btn btn-outline-secondary" id="selectSystemCustomerBtn">
<i class="bi bi-person"></i> Выбрать системного клиента (анонимный)
</button>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
</div>
</div>
</div>
</div>
<!-- Модалка: Создание нового клиента -->
<div class="modal fade" id="createCustomerModal" tabindex="-1" aria-labelledby="createCustomerModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="createCustomerModalLabel">
<i class="bi bi-person-plus"></i> Создать клиента
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="newCustomerName" class="form-label">Имя *</label>
<input type="text" class="form-control" id="newCustomerName" placeholder="Введите имя клиента" required>
</div>
<div class="mb-3">
<label for="newCustomerPhone" class="form-label">Телефон</label>
<input type="text" class="form-control" id="newCustomerPhone" placeholder="+375XXXXXXXXX">
</div>
<div class="mb-3">
<label for="newCustomerEmail" class="form-label">Email</label>
<input type="email" class="form-control" id="newCustomerEmail" placeholder="email@example.com">
</div>
<div id="createCustomerError" class="alert alert-danger d-none"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-primary" id="confirmCreateCustomerBtn">
<i class="bi bi-check-circle"></i> Создать
</button>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}
{% block extra_js %} {% block extra_js %}
@@ -401,6 +489,19 @@
<script id="categoriesData" type="application/json">{{ categories_json|safe }}</script> <script id="categoriesData" type="application/json">{{ categories_json|safe }}</script>
<script id="itemsData" type="application/json">{{ items_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="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="cartData" type="application/json">{{ cart_data|safe }}</script>
<script src="{% static 'pos/js/terminal.js' %}"></script> <script src="{% static 'pos/js/terminal.js' %}"></script>
{% endblock %} {% endblock %}

View File

@@ -9,6 +9,10 @@ urlpatterns = [
path('', views.pos_terminal, name='terminal'), path('', views.pos_terminal, name='terminal'),
# Установить текущий склад для POS (сохранение в сессии) [POST] # Установить текущий склад для POS (сохранение в сессии) [POST]
path('api/set-warehouse/<int:warehouse_id>/', views.set_warehouse, name='set-warehouse'), path('api/set-warehouse/<int:warehouse_id>/', views.set_warehouse, name='set-warehouse'),
# Установить текущего клиента для POS (сохранение в Redis с TTL 2 часа) [POST]
path('api/set-customer/<int:customer_id>/', views.set_customer, name='set-customer'),
# Сохранить корзину POS (сохранение в Redis с TTL 2 часа) [POST]
path('api/save-cart/', views.save_cart, name='save-cart'),
# Получить товары и комплекты (пагинация, поиск, сортировка) [GET] # Получить товары и комплекты (пагинация, поиск, сортировка) [GET]
path('api/items/', views.get_items_api, name='items-api'), path('api/items/', views.get_items_api, name='items-api'),
# Получить список активных витрин [GET] # Получить список активных витрин [GET]

View File

@@ -158,6 +158,9 @@ def pos_terminal(request):
Товары загружаются прогрессивно через API при клике на категорию. Товары загружаются прогрессивно через API при клике на категорию.
Работает только с одним выбранным складом. Работает только с одним выбранным складом.
""" """
from customers.models import Customer
from django.core.cache import cache
# Получаем текущий склад для POS # Получаем текущий склад для POS
current_warehouse = get_pos_warehouse(request) current_warehouse = get_pos_warehouse(request)
@@ -175,6 +178,56 @@ def pos_terminal(request):
} }
return render(request, 'pos/terminal.html', context) return render(request, 'pos/terminal.html', context)
# Получаем или создаём системного клиента
system_customer, _ = Customer.get_or_create_system_customer()
# Пытаемся получить сохраненного клиента из Redis
selected_customer = None
redis_key = f'pos:customer:{request.user.id}:{current_warehouse.id}'
cached_customer_data = cache.get(redis_key)
if cached_customer_data:
# Проверяем что клиент еще существует в БД
try:
customer = Customer.objects.get(id=cached_customer_data['customer_id'])
selected_customer = {
'id': customer.id,
'name': customer.name
}
except Customer.DoesNotExist:
# Клиент был удален - очищаем кэш
cache.delete(redis_key)
# Если нет сохраненного клиента - используем системного
if not selected_customer:
selected_customer = {
'id': system_customer.id,
'name': system_customer.name
}
# Пытаемся получить сохраненную корзину из Redis
cart_redis_key = f'pos:cart:{request.user.id}:{current_warehouse.id}'
cached_cart_data = cache.get(cart_redis_key)
cart_data = {}
if cached_cart_data:
# Валидируем товары и комплекты в корзине
from products.models import Product, ProductKit
for cart_key, item in cached_cart_data.items():
try:
if item['type'] == 'product':
# Проверяем что товар существует
Product.objects.get(id=item['id'])
cart_data[cart_key] = item
elif item['type'] in ('kit', 'showcase_kit'):
# Проверяем что комплект существует
ProductKit.objects.get(id=item['id'])
cart_data[cart_key] = item
except (Product.DoesNotExist, ProductKit.DoesNotExist):
# Товар или комплект удален - пропускаем
continue
# Загружаем только категории # Загружаем только категории
categories_qs = ProductCategory.objects.filter(is_active=True) categories_qs = ProductCategory.objects.filter(is_active=True)
categories = [{'id': c.id, 'name': c.name} for c in categories_qs] categories = [{'id': c.id, 'name': c.name} for c in categories_qs]
@@ -196,11 +249,92 @@ def pos_terminal(request):
'name': current_warehouse.name 'name': current_warehouse.name
}, },
'warehouses': warehouses_list, 'warehouses': warehouses_list,
'system_customer': {
'id': system_customer.id,
'name': system_customer.name
},
'selected_customer': selected_customer, # Текущий выбранный клиент (из Redis или системный)
'cart_data': json.dumps(cart_data), # Сохраненная корзина из Redis
'title': 'POS Terminal', 'title': 'POS Terminal',
} }
return render(request, 'pos/terminal.html', context) return render(request, 'pos/terminal.html', context)
@login_required
@require_http_methods(["POST"])
def save_cart(request):
"""
Сохранить корзину POS в Redis для текущего пользователя и склада.
TTL: 2 часа (7200 секунд)
"""
from django.core.cache import cache
import json
# Получаем текущий склад
current_warehouse = get_pos_warehouse(request)
if not current_warehouse:
return JsonResponse({'success': False, 'error': 'Не выбран активный склад'}, status=400)
try:
# Получаем данные корзины из тела запроса
body = json.loads(request.body)
cart_data = body.get('cart', {})
# Валидация структуры данных корзины
if not isinstance(cart_data, dict):
return JsonResponse({'success': False, 'error': 'Неверный формат данных корзины'}, status=400)
# Сохраняем в Redis
redis_key = f'pos:cart:{request.user.id}:{current_warehouse.id}'
cache.set(redis_key, cart_data, timeout=7200) # 2 часа
return JsonResponse({
'success': True,
'items_count': len(cart_data)
})
except json.JSONDecodeError:
return JsonResponse({'success': False, 'error': 'Неверный формат JSON'}, status=400)
except Exception as e:
return JsonResponse({'success': False, 'error': str(e)}, status=500)
@login_required
@require_http_methods(["POST"])
def set_customer(request, customer_id):
"""
Сохранить выбранного клиента в Redis для текущего пользователя и склада.
TTL: 2 часа (7200 секунд)
"""
from customers.models import Customer
from django.core.cache import cache
# Получаем текущий склад
current_warehouse = get_pos_warehouse(request)
if not current_warehouse:
return JsonResponse({'success': False, 'error': 'Не выбран активный склад'}, status=400)
# Проверяем, что клиент существует
try:
customer = Customer.objects.get(id=customer_id)
except Customer.DoesNotExist:
return JsonResponse({'success': False, 'error': 'Клиент не найден'}, status=404)
# Сохраняем в Redis
redis_key = f'pos:customer:{request.user.id}:{current_warehouse.id}'
customer_data = {
'customer_id': customer.id,
'customer_name': customer.name
}
cache.set(redis_key, customer_data, timeout=7200) # 2 часа
return JsonResponse({
'success': True,
'customer_id': customer.id,
'customer_name': customer.name
})
@login_required @login_required
@require_http_methods(["POST"]) @require_http_methods(["POST"])
def set_warehouse(request, warehouse_id): def set_warehouse(request, warehouse_id):