Интегрирован компонент поиска товаров в форму списания
- В WriteOffCreateView добавлена передача категорий и тегов в контекст - Шаблон writeoff_form.html обновлен с использованием product_search_picker - Автоматическая фильтрация партий по выбранному товару - Отображение информации о выбранном товаре с фото - Улучшенный UX при выборе товара для списания - Подключены CSS и JS компонента поиска товаров
This commit is contained in:
@@ -1,107 +1,156 @@
|
||||
{% 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>
|
||||
<div class="card-body">
|
||||
<!-- Ошибки формы -->
|
||||
{% if form.non_field_errors %}
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<strong><i class="bi bi-exclamation-triangle"></i> Ошибка:</strong>
|
||||
{% for error in form.non_field_errors %}
|
||||
<div>{{ error }}</div>
|
||||
{% endfor %}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
<!-- 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 %}
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<strong><i class="bi bi-exclamation-triangle"></i> Ошибка:</strong>
|
||||
{% for error in form.non_field_errors %}
|
||||
<div>{{ error }}</div>
|
||||
{% endfor %}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Выбранный товар -->
|
||||
<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>
|
||||
{{ form.batch }}
|
||||
{% 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">
|
||||
Остаток в партии: <strong id="batch-quantity">0</strong> шт
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Поле Количество -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ form.quantity.label }} <span class="text-danger">*</span></label>
|
||||
{{ form.quantity|smart_quantity }}
|
||||
{% if form.quantity.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.quantity.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<small class="text-muted d-block mt-1">
|
||||
Введите количество товара для списания
|
||||
</small>
|
||||
<!-- Предупреждение о превышении остатка -->
|
||||
<div id="quantity-warning" class="alert alert-warning mt-2" style="display:none;">
|
||||
<i class="bi bi-exclamation-circle"></i>
|
||||
<strong>Внимание!</strong> Вы пытаетесь списать <strong id="warning-qty">0</strong> шт,
|
||||
а в партии только <strong id="warning-batch">0</strong> шт.
|
||||
Недостаток: <strong id="warning-shortage" class="text-danger">0</strong> шт.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Поле Причина -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ form.reason.label }}</label>
|
||||
{{ form.reason }}
|
||||
{% if form.reason.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.reason.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Поле Номер документа -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ form.document_number.label }}</label>
|
||||
{{ form.document_number }}
|
||||
{% if form.document_number.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.document_number.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Поле Примечания -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ form.notes.label }}</label>
|
||||
{{ form.notes }}
|
||||
{% if form.notes.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.notes.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Кнопки действия -->
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" novalidate>
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Поле Партия -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ form.batch.label }} <span class="text-danger">*</span></label>
|
||||
{{ form.batch }}
|
||||
{% if form.batch.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.batch.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<!-- Информация об остатке партии -->
|
||||
<div id="batch-info" class="mt-2 p-2 bg-light border rounded" style="display:none;">
|
||||
<small class="text-muted">
|
||||
Остаток в партии: <strong id="batch-quantity">0</strong> шт
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Поле Количество -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ form.quantity.label }} <span class="text-danger">*</span></label>
|
||||
{{ form.quantity|smart_quantity }}
|
||||
{% if form.quantity.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.quantity.errors.0 }}</div>
|
||||
{% endif %}
|
||||
<small class="text-muted d-block mt-1">
|
||||
Введите количество товара для списания
|
||||
</small>
|
||||
<!-- Предупреждение о превышении остатка -->
|
||||
<div id="quantity-warning" class="alert alert-warning mt-2" style="display:none;">
|
||||
<i class="bi bi-exclamation-circle"></i>
|
||||
<strong>Внимание!</strong> Вы пытаетесь списать <strong id="warning-qty">0</strong> шт,
|
||||
а в партии только <strong id="warning-batch">0</strong> шт.
|
||||
Недостаток: <strong id="warning-shortage" class="text-danger">0</strong> шт.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Поле Причина -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ form.reason.label }}</label>
|
||||
{{ form.reason }}
|
||||
{% if form.reason.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.reason.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Поле Номер документа -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ form.document_number.label }}</label>
|
||||
{{ form.document_number }}
|
||||
{% if form.document_number.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.document_number.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Поле Примечания -->
|
||||
<div class="mb-3">
|
||||
<label class="form-label">{{ form.notes.label }}</label>
|
||||
{{ form.notes }}
|
||||
{% if form.notes.errors %}
|
||||
<div class="invalid-feedback d-block">{{ form.notes.errors.0 }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Кнопки действия -->
|
||||
<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>
|
||||
</div>
|
||||
</form>
|
||||
</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() {
|
||||
|
||||
Reference in New Issue
Block a user