Интегрирован компонент поиска товаров в форму списания
- В WriteOffCreateView добавлена передача категорий и тегов в контекст - Шаблон writeoff_form.html обновлен с использованием product_search_picker - Автоматическая фильтрация партий по выбранному товару - Отображение информации о выбранном товаре с фото - Улучшенный UX при выборе товара для списания - Подключены CSS и JS компонента поиска товаров
This commit is contained in:
@@ -1,10 +1,27 @@
|
|||||||
{% extends 'inventory/base_inventory_minimal.html' %}
|
{% extends 'inventory/base_inventory_minimal.html' %}
|
||||||
{% load inventory_filters %}
|
{% load static inventory_filters %}
|
||||||
{% block inventory_title %}{% if form.instance.pk %}Редактирование списания{% else %}Новое списание{% endif %}{% endblock %}
|
{% block inventory_title %}{% if form.instance.pk %}Редактирование списания{% else %}Новое списание{% endif %}{% endblock %}
|
||||||
{% block breadcrumb_current %}Списания{% endblock %}
|
{% block breadcrumb_current %}Списания{% endblock %}
|
||||||
|
|
||||||
{% block inventory_content %}
|
{% block inventory_content %}
|
||||||
|
<!-- 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">
|
||||||
<div class="card-header"><h4 class="mb-0">{% if form.instance.pk %}Редактирование{% else %}Создание{% endif %} списания</h4></div>
|
<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">
|
<div class="card-body">
|
||||||
<!-- Ошибки формы -->
|
<!-- Ошибки формы -->
|
||||||
{% if form.non_field_errors %}
|
{% if form.non_field_errors %}
|
||||||
@@ -17,9 +34,26 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% 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 %}
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<!-- Скрытое поле для хранения выбранного product_id -->
|
||||||
|
<input type="hidden" id="selected-product-id" name="selected_product_id" value="">
|
||||||
|
|
||||||
<!-- Поле Партия -->
|
<!-- Поле Партия -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">{{ form.batch.label }} <span class="text-danger">*</span></label>
|
<label class="form-label">{{ form.batch.label }} <span class="text-danger">*</span></label>
|
||||||
@@ -27,6 +61,9 @@
|
|||||||
{% if form.batch.errors %}
|
{% if form.batch.errors %}
|
||||||
<div class="invalid-feedback d-block">{{ form.batch.errors.0 }}</div>
|
<div class="invalid-feedback d-block">{{ form.batch.errors.0 }}</div>
|
||||||
{% endif %}
|
{% 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;">
|
<div id="batch-info" class="mt-2 p-2 bg-light border rounded" style="display:none;">
|
||||||
<small class="text-muted">
|
<small class="text-muted">
|
||||||
@@ -83,25 +120,37 @@
|
|||||||
|
|
||||||
<!-- Кнопки действия -->
|
<!-- Кнопки действия -->
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<button type="submit" class="btn btn-primary" id="submit-btn"><i class="bi bi-check-circle"></i> Сохранить</button>
|
<button type="submit" class="btn btn-primary" id="submit-btn">
|
||||||
<a href="{% url 'inventory:writeoff-list' %}" class="btn btn-secondary"><i class="bi bi-x-circle"></i> Отмена</a>
|
<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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
select, textarea, input {
|
select, textarea, input[type="text"], input[type="number"] {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.invalid-feedback {
|
.invalid-feedback {
|
||||||
color: #dc3545;
|
color: #dc3545;
|
||||||
font-size: 0.875em;
|
font-size: 0.875em;
|
||||||
}
|
}
|
||||||
|
#selected-product-info {
|
||||||
|
border-left: 4px solid #0d6efd;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<!-- JS для компонента -->
|
||||||
|
<script src="{% static 'products/js/product-search-picker.js' %}"></script>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Элементы формы
|
||||||
const batchSelect = document.querySelector('#id_batch');
|
const batchSelect = document.querySelector('#id_batch');
|
||||||
const quantityInput = document.querySelector('#id_quantity');
|
const quantityInput = document.querySelector('#id_quantity');
|
||||||
const batchInfo = document.getElementById('batch-info');
|
const batchInfo = document.getElementById('batch-info');
|
||||||
@@ -110,6 +159,116 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const warningQty = document.getElementById('warning-qty');
|
const warningQty = document.getElementById('warning-qty');
|
||||||
const warningBatch = document.getElementById('warning-batch');
|
const warningBatch = document.getElementById('warning-batch');
|
||||||
const warningShortage = document.getElementById('warning-shortage');
|
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() {
|
function getBatchQuantity() {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from django.contrib.auth.mixins import LoginRequiredMixin
|
|||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from ..models import WriteOff
|
from ..models import WriteOff
|
||||||
from ..forms import WriteOffForm
|
from ..forms import WriteOffForm
|
||||||
|
from products.models import ProductCategory, ProductTag
|
||||||
|
|
||||||
|
|
||||||
class WriteOffListView(LoginRequiredMixin, ListView):
|
class WriteOffListView(LoginRequiredMixin, ListView):
|
||||||
@@ -27,6 +28,12 @@ class WriteOffCreateView(LoginRequiredMixin, CreateView):
|
|||||||
template_name = 'inventory/writeoff/writeoff_form.html'
|
template_name = 'inventory/writeoff/writeoff_form.html'
|
||||||
success_url = reverse_lazy('inventory:writeoff-list')
|
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):
|
def form_valid(self, form):
|
||||||
messages.success(self.request, f'Списание товара успешно создано.')
|
messages.success(self.request, f'Списание товара успешно создано.')
|
||||||
return super().form_valid(form)
|
return super().form_valid(form)
|
||||||
|
|||||||
Reference in New Issue
Block a user