Интегрирован компонент поиска товаров в документы списания с фильтром по складу
- Добавлен параметр warehouse в API search_products_and_variants - API фильтрует товары по наличию на указанном складе через Stock - Обновлен _apply_product_filters для поддержки warehouse_id - ProductSearchPicker теперь поддерживает data-warehouse-id - Warehouse автоматически передается в AJAX запросы - В WriteOffDocumentDetailView добавлены categories и tags в контекст - Компонент поиска встроен в detail.html с жестким фильтром по складу документа - Single-select режим для выбора одного товара - JS автоматически заполняет select формы при выборе товара - Отображение выбранного товара с фото и артикулом - Автофокус на поле количества после выбора товара - Пользователь видит только товары доступные на складе документа
This commit is contained in:
@@ -1,8 +1,12 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Документ списания {{ document.document_number }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- CSS для компонента поиска -->
|
||||
<link rel="stylesheet" href="{% static 'products/css/product-search-picker.css' %}">
|
||||
|
||||
<div class="container-fluid px-4 py-3">
|
||||
<!-- Breadcrumbs -->
|
||||
<nav aria-label="breadcrumb" class="mb-2">
|
||||
@@ -162,12 +166,32 @@
|
||||
<!-- Боковая панель: добавление позиции -->
|
||||
{% if document.can_edit %}
|
||||
<div class="col-lg-4">
|
||||
<!-- Компонент поиска товаров -->
|
||||
<div class="mb-3">
|
||||
{% include 'products/components/product_search_picker.html' with container_id='writeoff-document-picker' title='Поиск товара для списания' warehouse_id=document.warehouse.id filter_in_stock_only=True categories=categories tags=tags add_button_text='Выбрать товар' multi_select=False show_select_all=False content_height='300px' %}
|
||||
</div>
|
||||
|
||||
<!-- Форма добавления позиции -->
|
||||
<div class="card border-0 shadow-sm sticky-top" style="top: 1rem;">
|
||||
<div class="card-header bg-light py-3">
|
||||
<h6 class="mb-0"><i class="bi bi-plus-circle me-2"></i>Добавить товар</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" action="{% url 'inventory:writeoff-document-add-item' document.pk %}">
|
||||
<!-- Информация о выбранном товаре -->
|
||||
<div id="selected-product-info" class="alert alert-info mb-3" style="display: none;">
|
||||
<div class="d-flex align-items-center">
|
||||
<img id="selected-product-photo" src="" alt="" class="rounded me-2" style="width: 40px; height: 40px; object-fit: cover; display: none;">
|
||||
<div class="flex-grow-1">
|
||||
<strong id="selected-product-name" class="d-block"></strong>
|
||||
<small class="text-muted" id="selected-product-sku"></small>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-outline-secondary" id="clear-selected-product" title="Очистить">
|
||||
<i class="bi bi-x"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form method="post" action="{% url 'inventory:writeoff-document-add-item' document.pk %}" id="add-item-form">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="mb-3">
|
||||
@@ -176,6 +200,9 @@
|
||||
{% if item_form.product.errors %}
|
||||
<div class="text-danger small">{{ item_form.product.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<small class="text-muted d-block mt-1">
|
||||
<i class="bi bi-info-circle"></i> Используйте поиск выше для удобного выбора
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
@@ -206,4 +233,81 @@
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JS для компонента поиска -->
|
||||
<script src="{% static 'products/js/product-search-picker.js' %}"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Элементы формы
|
||||
const productSelect = document.querySelector('#id_product');
|
||||
const quantityInput = document.querySelector('#id_quantity');
|
||||
|
||||
// Элементы отображения выбранного товара
|
||||
const selectedProductInfo = document.getElementById('selected-product-info');
|
||||
const selectedProductName = document.getElementById('selected-product-name');
|
||||
const selectedProductSku = document.getElementById('selected-product-sku');
|
||||
const selectedProductPhoto = document.getElementById('selected-product-photo');
|
||||
const clearSelectedBtn = document.getElementById('clear-selected-product');
|
||||
|
||||
// Инициализация компонента поиска товаров
|
||||
const picker = ProductSearchPicker.init('#writeoff-document-picker', {
|
||||
onAddSelected: function(products, instance) {
|
||||
if (products.length > 0) {
|
||||
selectProduct(products[0]);
|
||||
instance.clearSelection();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Функция выбора товара
|
||||
function selectProduct(product) {
|
||||
const productId = String(product.id).replace('product_', '');
|
||||
const productName = product.text || product.name || '';
|
||||
|
||||
// Показываем информацию о выбранном товаре
|
||||
selectedProductName.textContent = productName;
|
||||
selectedProductSku.textContent = product.sku || '';
|
||||
|
||||
if (product.photo_url) {
|
||||
selectedProductPhoto.src = product.photo_url;
|
||||
selectedProductPhoto.style.display = 'block';
|
||||
} else {
|
||||
selectedProductPhoto.style.display = 'none';
|
||||
}
|
||||
|
||||
selectedProductInfo.style.display = 'block';
|
||||
|
||||
// Устанавливаем значение в select формы
|
||||
if (productSelect) {
|
||||
productSelect.value = productId;
|
||||
// Триггерим change для Select2, если он используется
|
||||
const event = new Event('change', { bubbles: true });
|
||||
productSelect.dispatchEvent(event);
|
||||
}
|
||||
|
||||
// Фокус в поле количества
|
||||
if (quantityInput) {
|
||||
quantityInput.focus();
|
||||
quantityInput.select();
|
||||
}
|
||||
}
|
||||
|
||||
// Функция очистки выбора товара
|
||||
function clearSelectedProduct() {
|
||||
selectedProductInfo.style.display = 'none';
|
||||
selectedProductName.textContent = '';
|
||||
selectedProductSku.textContent = '';
|
||||
selectedProductPhoto.style.display = 'none';
|
||||
|
||||
if (productSelect) {
|
||||
productSelect.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
// Очистка выбора товара
|
||||
if (clearSelectedBtn) {
|
||||
clearSelectedBtn.addEventListener('click', clearSelectedProduct);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -60,6 +60,12 @@ class WriteOffDocumentDetailView(LoginRequiredMixin, DetailView):
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['item_form'] = WriteOffDocumentItemForm(document=self.object)
|
||||
|
||||
# Добавляем категории и теги для компонента поиска товаров
|
||||
from products.models import ProductCategory, ProductTag
|
||||
context['categories'] = ProductCategory.objects.filter(is_active=True).order_by('name')
|
||||
context['tags'] = ProductTag.objects.filter(is_active=True).order_by('name')
|
||||
|
||||
return context
|
||||
|
||||
|
||||
|
||||
@@ -64,7 +64,8 @@
|
||||
search: '',
|
||||
category: '',
|
||||
tag: '',
|
||||
inStock: false
|
||||
inStock: false,
|
||||
warehouse: '' // ID склада для фильтрации
|
||||
}
|
||||
};
|
||||
|
||||
@@ -106,6 +107,11 @@
|
||||
this.state.currentView = initialView;
|
||||
this._updateViewButtons();
|
||||
}
|
||||
|
||||
// Инициализация warehouse из data-атрибута
|
||||
if (this.container.dataset.warehouseId) {
|
||||
this.state.filters.warehouse = this.container.dataset.warehouseId;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -278,6 +284,9 @@
|
||||
if (this.state.filters.inStock) {
|
||||
params.append('in_stock', 'true');
|
||||
}
|
||||
if (this.state.filters.warehouse) {
|
||||
params.append('warehouse', this.state.filters.warehouse);
|
||||
}
|
||||
|
||||
fetch(this.options.apiUrl + '?' + params.toString())
|
||||
.then(function(response) {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
- multi_select: множественный выбор (default: True)
|
||||
- max_selection: максимальное количество выбранных товаров (default: null)
|
||||
- filter_in_stock_only: показывать только товары в наличии (default: False)
|
||||
- warehouse_id: ID склада для фильтрации товаров (default: None)
|
||||
- categories: список категорий для фильтра (queryset или list)
|
||||
- tags: список тегов для фильтра (queryset или list)
|
||||
- content_height: высота контейнера с товарами (default: '400px')
|
||||
@@ -46,7 +47,8 @@ ProductSearchPicker.init('#writeoff-products', {
|
||||
data-api-url="{{ api_url|default:'/products/api/search-products-variants/' }}"
|
||||
data-multi-select="{{ multi_select|default:'true' }}"
|
||||
data-max-selection="{{ max_selection|default:'' }}"
|
||||
data-exclude-kits="true">
|
||||
data-exclude-kits="true"
|
||||
{% if warehouse_id %}data-warehouse-id="{{ warehouse_id }}"{% endif %}>
|
||||
|
||||
<div class="card shadow-sm">
|
||||
<!-- Заголовок -->
|
||||
|
||||
@@ -20,7 +20,7 @@ def _get_product_photo_url(product_id):
|
||||
return None
|
||||
|
||||
|
||||
def _apply_product_filters(queryset, category_id=None, tag_id=None, in_stock_only=False):
|
||||
def _apply_product_filters(queryset, category_id=None, tag_id=None, in_stock_only=False, warehouse_id=None):
|
||||
"""Применяет фильтры к queryset товаров."""
|
||||
if category_id:
|
||||
queryset = queryset.filter(categories__id=category_id)
|
||||
@@ -28,6 +28,14 @@ def _apply_product_filters(queryset, category_id=None, tag_id=None, in_stock_onl
|
||||
queryset = queryset.filter(tags__id=tag_id)
|
||||
if in_stock_only:
|
||||
queryset = queryset.filter(in_stock=True)
|
||||
if warehouse_id:
|
||||
# Фильтруем только товары, которые есть на указанном складе с доступным количеством
|
||||
from inventory.models import Stock
|
||||
products_with_stock = Stock.objects.filter(
|
||||
warehouse_id=warehouse_id,
|
||||
quantity_available__gt=0
|
||||
).values_list('product_id', flat=True)
|
||||
queryset = queryset.filter(id__in=products_with_stock)
|
||||
return queryset.distinct()
|
||||
|
||||
|
||||
@@ -44,6 +52,7 @@ def search_products_and_variants(request):
|
||||
- category: ID категории для фильтрации (опционально)
|
||||
- tag: ID тега для фильтрации (опционально)
|
||||
- in_stock: 'true' для фильтрации только товаров в наличии (опционально)
|
||||
- warehouse: ID склада для фильтрации только товаров с доступным остатком (опционально)
|
||||
|
||||
Возвращает JSON в формате Select2 с группировкой:
|
||||
{
|
||||
@@ -156,11 +165,12 @@ def search_products_and_variants(request):
|
||||
category_id = request.GET.get('category', '').strip()
|
||||
tag_id = request.GET.get('tag', '').strip()
|
||||
in_stock_only = request.GET.get('in_stock', '').lower() == 'true'
|
||||
warehouse_id = request.GET.get('warehouse', '').strip()
|
||||
|
||||
results = []
|
||||
|
||||
# Проверяем, есть ли дополнительные фильтры
|
||||
has_filters = category_id or tag_id or in_stock_only
|
||||
has_filters = category_id or tag_id or in_stock_only or warehouse_id
|
||||
|
||||
# Если поиска нет - показываем популярные товары и комплекты
|
||||
if not query or len(query) < 2:
|
||||
@@ -178,7 +188,7 @@ def search_products_and_variants(request):
|
||||
# Показываем последние добавленные активные товары
|
||||
products_qs = Product.objects.filter(status='active')
|
||||
# Применяем фильтры
|
||||
products_qs = _apply_product_filters(products_qs, category_id, tag_id, in_stock_only)
|
||||
products_qs = _apply_product_filters(products_qs, category_id, tag_id, in_stock_only, warehouse_id)
|
||||
products = products_qs.order_by('-created_at')[:page_size]\
|
||||
.values('id', 'name', 'sku', 'price', 'sale_price', 'in_stock')
|
||||
|
||||
@@ -305,7 +315,7 @@ def search_products_and_variants(request):
|
||||
).order_by('-relevance', 'name')
|
||||
|
||||
# Применяем дополнительные фильтры
|
||||
products_query = _apply_product_filters(products_query, category_id, tag_id, in_stock_only)
|
||||
products_query = _apply_product_filters(products_query, category_id, tag_id, in_stock_only, warehouse_id)
|
||||
|
||||
total_products = products_query.count()
|
||||
start = (page - 1) * page_size
|
||||
|
||||
Reference in New Issue
Block a user