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

- В WriteOffCreateView добавлена передача категорий и тегов в контекст
- Шаблон writeoff_form.html обновлен с использованием product_search_picker
- Автоматическая фильтрация партий по выбранному товару
- Отображение информации о выбранном товаре с фото
- Улучшенный UX при выборе товара для списания
- Подключены CSS и JS компонента поиска товаров
This commit is contained in:
2025-12-10 23:36:58 +03:00
parent f8808c6ba0
commit 865cdbbb8b
2 changed files with 251 additions and 85 deletions

View File

@@ -1,10 +1,27 @@
{% extends 'inventory/base_inventory_minimal.html' %}
{% load inventory_filters %}
{% load static inventory_filters %}
{% block inventory_title %}{% if form.instance.pk %}Редактирование списания{% else %}Новое списание{% endif %}{% endblock %}
{% block breadcrumb_current %}Списания{% endblock %}
{% block inventory_content %}
<div class="card">
<div class="card-header"><h4 class="mb-0">{% if form.instance.pk %}Редактирование{% else %}Создание{% endif %} списания</h4></div>
<!-- CSS для компонента -->
<link rel="stylesheet" href="{% static 'products/css/product-search-picker.css' %}">
<div class="row">
<!-- Левая колонка: поиск товаров -->
<div class="col-lg-6 mb-4">
{% include 'products/components/product_search_picker.html' with container_id='writeoff-product-picker' title='Найти товар для списания' filter_in_stock_only=True categories=categories tags=tags add_button_text='Выбрать товар' multi_select=False content_height='350px' %}
</div>
<!-- Правая колонка: форма списания -->
<div class="col-lg-6">
<div class="card">
<div class="card-header">
<h5 class="mb-0">
<i class="bi bi-clipboard-minus text-danger"></i>
{% if form.instance.pk %}Редактирование{% else %}Создание{% endif %} списания
</h5>
</div>
<div class="card-body">
<!-- Ошибки формы -->
{% if form.non_field_errors %}
@@ -17,9 +34,26 @@
</div>
{% endif %}
<form method="post" novalidate>
<!-- Выбранный товар -->
<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-3" style="width: 50px; height: 50px; object-fit: cover; display: none;">
<div class="flex-grow-1">
<strong id="selected-product-name"></strong>
<div class="small text-muted" id="selected-product-sku"></div>
</div>
<button type="button" class="btn btn-sm btn-outline-secondary" id="clear-selected-product">
<i class="bi bi-x"></i>
</button>
</div>
</div>
<form method="post" novalidate id="writeoff-form">
{% csrf_token %}
<!-- Скрытое поле для хранения выбранного product_id -->
<input type="hidden" id="selected-product-id" name="selected_product_id" value="">
<!-- Поле Партия -->
<div class="mb-3">
<label class="form-label">{{ form.batch.label }} <span class="text-danger">*</span></label>
@@ -27,6 +61,9 @@
{% if form.batch.errors %}
<div class="invalid-feedback d-block">{{ form.batch.errors.0 }}</div>
{% endif %}
<small class="text-muted" id="batch-hint">
<i class="bi bi-info-circle"></i> Сначала выберите товар слева, затем здесь появятся его партии
</small>
<!-- Информация об остатке партии -->
<div id="batch-info" class="mt-2 p-2 bg-light border rounded" style="display:none;">
<small class="text-muted">
@@ -83,25 +120,37 @@
<!-- Кнопки действия -->
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary" id="submit-btn"><i class="bi bi-check-circle"></i> Сохранить</button>
<a href="{% url 'inventory:writeoff-list' %}" class="btn btn-secondary"><i class="bi bi-x-circle"></i> Отмена</a>
<button type="submit" class="btn btn-primary" id="submit-btn">
<i class="bi bi-check-circle"></i> Сохранить
</button>
<a href="{% url 'inventory:writeoff-list' %}" class="btn btn-secondary">
<i class="bi bi-x-circle"></i> Отмена
</a>
</div>
</form>
</div>
</div>
</div>
</div>
<style>
select, textarea, input {
select, textarea, input[type="text"], input[type="number"] {
width: 100%;
}
.invalid-feedback {
color: #dc3545;
font-size: 0.875em;
}
#selected-product-info {
border-left: 4px solid #0d6efd;
}
</style>
<!-- JS для компонента -->
<script src="{% static 'products/js/product-search-picker.js' %}"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Элементы формы
const batchSelect = document.querySelector('#id_batch');
const quantityInput = document.querySelector('#id_quantity');
const batchInfo = document.getElementById('batch-info');
@@ -110,6 +159,116 @@ document.addEventListener('DOMContentLoaded', function() {
const warningQty = document.getElementById('warning-qty');
const warningBatch = document.getElementById('warning-batch');
const warningShortage = document.getElementById('warning-shortage');
const batchHint = document.getElementById('batch-hint');
// Элементы отображения выбранного товара
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 selectedProductIdInput = document.getElementById('selected-product-id');
const clearSelectedBtn = document.getElementById('clear-selected-product');
// Сохраняем все опции партий для фильтрации
const allBatchOptions = Array.from(batchSelect.options).map(opt => ({
value: opt.value,
text: opt.text,
productName: opt.text.split(' на ')[0] // Извлекаем название товара
}));
// Инициализация компонента поиска товаров
const picker = ProductSearchPicker.init('#writeoff-product-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 || '';
selectedProductIdInput.value = productId;
if (product.photo_url) {
selectedProductPhoto.src = product.photo_url;
selectedProductPhoto.style.display = 'block';
} else {
selectedProductPhoto.style.display = 'none';
}
selectedProductInfo.style.display = 'block';
// Фильтруем партии по выбранному товару
filterBatchesByProduct(productName);
// Скрываем подсказку
batchHint.style.display = 'none';
}
// Функция очистки выбора товара
function clearSelectedProduct() {
selectedProductInfo.style.display = 'none';
selectedProductName.textContent = '';
selectedProductSku.textContent = '';
selectedProductPhoto.style.display = 'none';
selectedProductIdInput.value = '';
// Показываем все партии
showAllBatches();
// Показываем подсказку
batchHint.style.display = 'block';
}
// Фильтрация партий по названию товара
function filterBatchesByProduct(productName) {
// Очищаем select
batchSelect.innerHTML = '<option value="">---------</option>';
// Добавляем только партии выбранного товара
const normalizedName = productName.toLowerCase().trim();
allBatchOptions.forEach(function(opt) {
if (opt.value && opt.productName.toLowerCase().trim().includes(normalizedName.split(' (')[0])) {
const option = document.createElement('option');
option.value = opt.value;
option.text = opt.text;
batchSelect.appendChild(option);
}
});
// Если найдена только одна партия, выбираем её автоматически
if (batchSelect.options.length === 2) {
batchSelect.selectedIndex = 1;
updateBatchInfo();
}
}
// Показать все партии
function showAllBatches() {
batchSelect.innerHTML = '<option value="">---------</option>';
allBatchOptions.forEach(function(opt) {
if (opt.value) {
const option = document.createElement('option');
option.value = opt.value;
option.text = opt.text;
batchSelect.appendChild(option);
}
});
}
// Очистка выбора товара
clearSelectedBtn.addEventListener('click', clearSelectedProduct);
// ===== Существующая логика для партий =====
// Функция для получения остатка партии
function getBatchQuantity() {

View File

@@ -9,6 +9,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib import messages
from ..models import WriteOff
from ..forms import WriteOffForm
from products.models import ProductCategory, ProductTag
class WriteOffListView(LoginRequiredMixin, ListView):
@@ -27,6 +28,12 @@ class WriteOffCreateView(LoginRequiredMixin, CreateView):
template_name = 'inventory/writeoff/writeoff_form.html'
success_url = reverse_lazy('inventory:writeoff-list')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['categories'] = ProductCategory.objects.filter(is_active=True).order_by('name')
context['tags'] = ProductTag.objects.filter(is_active=True).order_by('name')
return context
def form_valid(self, form):
messages.success(self.request, f'Списание товара успешно создано.')
return super().form_valid(form)