Интегрирован компонент поиска товаров в документы списания с фильтром по складу

- Добавлен параметр 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:
2025-12-11 00:02:37 +03:00
parent 542b90c3f1
commit 2e5ebabf22
5 changed files with 138 additions and 7 deletions

View File

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

View File

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