Реализован выбор склада для POS: добавлена логика выбора склада по умолчанию из сессии, эндпоинт смены склада, модалка выбора и отображение текущего склада
This commit is contained in:
@@ -826,6 +826,69 @@ document.getElementById('customerSelectBtn').addEventListener('click', () => {
|
|||||||
alert('Функция выбора клиента будет реализована позже');
|
alert('Функция выбора клиента будет реализована позже');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Смена склада
|
||||||
|
const changeWarehouseBtn = document.getElementById('changeWarehouseBtn');
|
||||||
|
if (changeWarehouseBtn) {
|
||||||
|
changeWarehouseBtn.addEventListener('click', () => {
|
||||||
|
const modal = new bootstrap.Modal(document.getElementById('selectWarehouseModal'));
|
||||||
|
modal.show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Обработка выбора склада из списка
|
||||||
|
document.addEventListener('click', async (e) => {
|
||||||
|
const warehouseItem = e.target.closest('.warehouse-item');
|
||||||
|
if (!warehouseItem) return;
|
||||||
|
|
||||||
|
const warehouseId = warehouseItem.dataset.warehouseId;
|
||||||
|
const warehouseName = warehouseItem.dataset.warehouseName;
|
||||||
|
|
||||||
|
// Проверяем, есть ли товары в корзине
|
||||||
|
if (cart.size > 0) {
|
||||||
|
const confirmed = confirm(`При смене склада корзина будет очищена.\n\nПереключиться на склад "${warehouseName}"?`);
|
||||||
|
if (!confirmed) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Отправляем запрос на смену склада
|
||||||
|
const response = await fetch(`/pos/api/set-warehouse/${warehouseId}/`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-CSRFToken': getCsrfToken()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (data.success) {
|
||||||
|
// Перезагружаем страницу для обновления данных
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
alert(`Ошибка: ${data.error}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при смене склада:', error);
|
||||||
|
alert('Произошла ошибка при смене склада');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Вспомогательная функция для получения CSRF токена
|
||||||
|
function getCsrfToken() {
|
||||||
|
const name = 'csrftoken';
|
||||||
|
let cookieValue = null;
|
||||||
|
if (document.cookie && document.cookie !== '') {
|
||||||
|
const cookies = document.cookie.split(';');
|
||||||
|
for (let i = 0; i < cookies.length; i++) {
|
||||||
|
const cookie = cookies[i].trim();
|
||||||
|
if (cookie.substring(0, name.length + 1) === (name + '=')) {
|
||||||
|
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cookieValue;
|
||||||
|
}
|
||||||
|
|
||||||
// Инициализация
|
// Инициализация
|
||||||
renderCategories();
|
renderCategories();
|
||||||
renderProducts();
|
renderProducts();
|
||||||
|
|||||||
@@ -32,6 +32,21 @@
|
|||||||
<!-- Right Panel (4/12) - Fixed -->
|
<!-- Right Panel (4/12) - Fixed -->
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="right-panel-fixed d-flex flex-column">
|
<div class="right-panel-fixed d-flex flex-column">
|
||||||
|
<!-- Информация о складе -->
|
||||||
|
{% if current_warehouse %}
|
||||||
|
<div class="card mb-2">
|
||||||
|
<div class="card-body py-2 px-3 d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<small class="text-muted d-block" style="font-size: 0.75rem;">Склад:</small>
|
||||||
|
<strong style="font-size: 0.95rem;">{{ current_warehouse.name }}</strong>
|
||||||
|
</div>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" id="changeWarehouseBtn" style="font-size: 0.75rem;">
|
||||||
|
<i class="bi bi-arrow-left-right"></i> Сменить
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- Cart Panel -->
|
<!-- Cart Panel -->
|
||||||
<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">
|
||||||
@@ -231,6 +246,42 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Модалка: Выбор склада -->
|
||||||
|
<div class="modal fade" id="selectWarehouseModal" tabindex="-1" aria-labelledby="selectWarehouseModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="selectWarehouseModalLabel">
|
||||||
|
<i class="bi bi-building"></i> Выбор склада
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="list-group" id="warehouseList">
|
||||||
|
{% for warehouse in warehouses %}
|
||||||
|
<button type="button" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center warehouse-item"
|
||||||
|
data-warehouse-id="{{ warehouse.id }}"
|
||||||
|
data-warehouse-name="{{ warehouse.name }}">
|
||||||
|
<div>
|
||||||
|
<strong>{{ warehouse.name }}</strong>
|
||||||
|
{% if warehouse.is_default %}
|
||||||
|
<span class="badge bg-warning text-dark ms-2">По умолчанию</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if warehouse.id == current_warehouse.id %}
|
||||||
|
<i class="bi bi-check-circle-fill text-success"></i>
|
||||||
|
{% endif %}
|
||||||
|
</button>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block extra_js %}
|
{% block extra_js %}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ app_name = 'pos'
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path('', views.pos_terminal, name='terminal'),
|
path('', views.pos_terminal, name='terminal'),
|
||||||
|
path('api/set-warehouse/<int:warehouse_id>/', views.set_warehouse, name='set-warehouse'),
|
||||||
path('api/showcase-items/', views.showcase_items_api, name='showcase-items-api'),
|
path('api/showcase-items/', views.showcase_items_api, name='showcase-items-api'),
|
||||||
path('api/get-showcases/', views.get_showcases_api, name='get-showcases-api'),
|
path('api/get-showcases/', views.get_showcases_api, name='get-showcases-api'),
|
||||||
path('api/showcase-kits/', views.get_showcase_kits_api, name='showcase-kits-api'),
|
path('api/showcase-kits/', views.get_showcase_kits_api, name='showcase-kits-api'),
|
||||||
|
|||||||
@@ -14,6 +14,38 @@ from inventory.models import Showcase, Reservation, Warehouse
|
|||||||
from inventory.services import ShowcaseManager
|
from inventory.services import ShowcaseManager
|
||||||
|
|
||||||
|
|
||||||
|
def get_pos_warehouse(request):
|
||||||
|
"""
|
||||||
|
Получить текущий склад для POS из сессии или выбрать дефолтный.
|
||||||
|
Логика выбора:
|
||||||
|
1. Если в сессии есть pos_warehouse_id - используем его
|
||||||
|
2. Иначе берем склад с is_default=True
|
||||||
|
3. Если нет is_default - берем первый активный
|
||||||
|
4. Если нет активных складов - None
|
||||||
|
"""
|
||||||
|
warehouse_id = request.session.get('pos_warehouse_id')
|
||||||
|
|
||||||
|
if warehouse_id:
|
||||||
|
try:
|
||||||
|
return Warehouse.objects.get(id=warehouse_id, is_active=True)
|
||||||
|
except Warehouse.DoesNotExist:
|
||||||
|
# Склад был удален или деактивирован - сбрасываем сессию
|
||||||
|
request.session.pop('pos_warehouse_id', None)
|
||||||
|
|
||||||
|
# Ищем склад по умолчанию
|
||||||
|
warehouse = Warehouse.objects.filter(is_active=True, is_default=True).first()
|
||||||
|
|
||||||
|
if not warehouse:
|
||||||
|
# Берем любой первый активный
|
||||||
|
warehouse = Warehouse.objects.filter(is_active=True).first()
|
||||||
|
|
||||||
|
# Сохраняем в сессию для следующих запросов
|
||||||
|
if warehouse:
|
||||||
|
request.session['pos_warehouse_id'] = warehouse.id
|
||||||
|
|
||||||
|
return warehouse
|
||||||
|
|
||||||
|
|
||||||
def get_showcase_kits_for_pos():
|
def get_showcase_kits_for_pos():
|
||||||
"""
|
"""
|
||||||
Получает витринные комплекты для отображения в POS.
|
Получает витринные комплекты для отображения в POS.
|
||||||
@@ -124,9 +156,27 @@ def pos_terminal(request):
|
|||||||
Tablet-friendly POS screen prototype.
|
Tablet-friendly POS screen prototype.
|
||||||
Shows categories and all items (products + kits) for quick tap-to-add.
|
Shows categories and all items (products + kits) for quick tap-to-add.
|
||||||
Оптимизировано: убрана стартовая загрузка витрин, только thumbnail фото.
|
Оптимизировано: убрана стартовая загрузка витрин, только thumbnail фото.
|
||||||
|
Работает только с одним выбранным складом.
|
||||||
"""
|
"""
|
||||||
from products.models import ProductPhoto, ProductKitPhoto
|
from products.models import ProductPhoto, ProductKitPhoto
|
||||||
|
|
||||||
|
# Получаем текущий склад для POS
|
||||||
|
current_warehouse = get_pos_warehouse(request)
|
||||||
|
|
||||||
|
if not current_warehouse:
|
||||||
|
# Нет активных складов - показываем ошибку
|
||||||
|
from django.contrib import messages
|
||||||
|
messages.error(request, 'Нет активных складов. Обратитесь к администратору.')
|
||||||
|
context = {
|
||||||
|
'categories_json': json.dumps([]),
|
||||||
|
'items_json': json.dumps([]),
|
||||||
|
'showcase_kits_json': json.dumps([]),
|
||||||
|
'current_warehouse': None,
|
||||||
|
'warehouses': [],
|
||||||
|
'title': 'POS Terminal',
|
||||||
|
}
|
||||||
|
return render(request, 'pos/terminal.html', context)
|
||||||
|
|
||||||
categories_qs = ProductCategory.objects.filter(is_active=True)
|
categories_qs = ProductCategory.objects.filter(is_active=True)
|
||||||
|
|
||||||
# Prefetch для первого фото товаров
|
# Prefetch для первого фото товаров
|
||||||
@@ -195,16 +245,52 @@ def pos_terminal(request):
|
|||||||
|
|
||||||
# Объединяем все позиции
|
# Объединяем все позиции
|
||||||
all_items = products + kits
|
all_items = products + kits
|
||||||
|
|
||||||
|
# Список всех активных складов для модалки выбора
|
||||||
|
warehouses = Warehouse.objects.filter(is_active=True).order_by('-is_default', 'name')
|
||||||
|
warehouses_list = [{
|
||||||
|
'id': w.id,
|
||||||
|
'name': w.name,
|
||||||
|
'is_default': w.is_default
|
||||||
|
} for w in warehouses]
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'categories_json': json.dumps(categories),
|
'categories_json': json.dumps(categories),
|
||||||
'items_json': json.dumps(all_items),
|
'items_json': json.dumps(all_items),
|
||||||
'showcase_kits_json': json.dumps([]), # Пустой массив - загрузка по API
|
'showcase_kits_json': json.dumps([]), # Пустой массив - загрузка по API
|
||||||
|
'current_warehouse': {
|
||||||
|
'id': current_warehouse.id,
|
||||||
|
'name': current_warehouse.name
|
||||||
|
},
|
||||||
|
'warehouses': warehouses_list,
|
||||||
'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 set_warehouse(request, warehouse_id):
|
||||||
|
"""
|
||||||
|
Установить текущий склад для POS.
|
||||||
|
Сохраняет выбор в сессию.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
warehouse = Warehouse.objects.get(id=warehouse_id, is_active=True)
|
||||||
|
request.session['pos_warehouse_id'] = warehouse.id
|
||||||
|
|
||||||
|
return JsonResponse({
|
||||||
|
'success': True,
|
||||||
|
'warehouse_id': warehouse.id,
|
||||||
|
'warehouse_name': warehouse.name
|
||||||
|
})
|
||||||
|
except Warehouse.DoesNotExist:
|
||||||
|
return JsonResponse({
|
||||||
|
'success': False,
|
||||||
|
'error': 'Склад не найден или неактивен'
|
||||||
|
}, status=404)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@require_http_methods(["GET"])
|
@require_http_methods(["GET"])
|
||||||
def showcase_items_api(request):
|
def showcase_items_api(request):
|
||||||
|
|||||||
Reference in New Issue
Block a user