Улучшения инвентаризации: автоматическое проведение документов, оптимизация запросов и улучшения UI
- Автоматическое проведение документов списания и оприходования после завершения инвентаризации - Оптимизация SQL-запросов: устранение N+1, bulk-операции для Stock, агрегация для StockBatch и Reservation - Изменение формулы расчета разницы: (quantity_fact + quantity_reserved) - quantity_available - Переименование поля 'По факту' в 'Подсчитано (факт, свободные)' - Добавлены столбцы 'В резервах' и 'Всего на складе' в таблицу инвентаризации - Перемещение столбца 'В системе (свободно)' после 'В резервах' с визуальным выделением - Центральное выравнивание значений в столбцах таблицы - Автоматическое выделение текста при фокусе на поле ввода количества - Исправление форматирования разницы (убраны лишние нули) - Изменение статуса 'Не обработана' на 'Не проведено' - Добавление номера документа для инвентаризаций (INV-XXXXXX) - Отображение всех типов списаний в debug-странице (WriteOff, WriteOffDocument, WriteOffDocumentItem) - Улучшение отображения документов в детальном просмотре инвентаризации с возможностью перехода к ним
This commit is contained in:
@@ -384,9 +384,9 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- СПИСАНИЯ (SaleBatchAllocation) -->
|
||||
<!-- СПИСАНИЯ ИЗ ПРОДАЖ (SaleBatchAllocation) -->
|
||||
<div class="section-card">
|
||||
<h3>📤 Списания SaleBatchAllocation ({{ allocations.count }})</h3>
|
||||
<h3>📤 Списания из продаж SaleBatchAllocation ({{ allocations.count }})</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered table-hover">
|
||||
<thead>
|
||||
@@ -410,7 +410,124 @@
|
||||
<td>{{ alloc.cost_price|floatformat:2 }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="6" class="text-center text-muted">Нет списаний</td></tr>
|
||||
<tr><td colspan="6" class="text-center text-muted">Нет списаний из продаж</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- СПИСАНИЯ WriteOff (одиночные записи) -->
|
||||
<div class="section-card">
|
||||
<h3>🗑️ Списания WriteOff ({{ writeoffs.count }})</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Товар</th>
|
||||
<th>Склад</th>
|
||||
<th>Партия ID</th>
|
||||
<th>Кол-во</th>
|
||||
<th>Причина</th>
|
||||
<th>Дата</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for wo in writeoffs %}
|
||||
<tr>
|
||||
<td>{{ wo.id }}</td>
|
||||
<td><strong>{{ wo.batch.product.name }}</strong></td>
|
||||
<td>{{ wo.batch.warehouse.name }}</td>
|
||||
<td>{{ wo.batch.id }}</td>
|
||||
<td><span class="badge bg-danger">{{ wo.quantity }}</span></td>
|
||||
<td>{{ wo.reason|default:"-" }}</td>
|
||||
<td class="text-muted-small">{{ wo.date|date:"d.m.Y H:i:s" }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="7" class="text-center text-muted">Нет списаний WriteOff</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ДОКУМЕНТЫ СПИСАНИЯ (WriteOffDocument) -->
|
||||
<div class="section-card">
|
||||
<h3>📄 Документы списания WriteOffDocument ({{ writeoff_documents.count }})</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Номер</th>
|
||||
<th>Склад</th>
|
||||
<th>Статус</th>
|
||||
<th>Дата</th>
|
||||
<th>Инвентаризация</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for doc in writeoff_documents %}
|
||||
<tr>
|
||||
<td>{{ doc.id }}</td>
|
||||
<td><strong>{{ doc.document_number|default:"-" }}</strong></td>
|
||||
<td>{{ doc.warehouse.name }}</td>
|
||||
<td>
|
||||
{% if doc.status == 'draft' %}
|
||||
<span class="badge bg-warning text-dark">Черновик</span>
|
||||
{% elif doc.status == 'confirmed' %}
|
||||
<span class="badge bg-success">Проведен</span>
|
||||
{% elif doc.status == 'cancelled' %}
|
||||
<span class="badge bg-danger">Отменен</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">{{ doc.status }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-muted-small">{{ doc.date|date:"d.m.Y" }}</td>
|
||||
<td>
|
||||
{% if doc.inventory %}
|
||||
<span class="badge bg-info">INV-{{ doc.inventory.id }}</span>
|
||||
{% else %}
|
||||
<span class="text-muted">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="6" class="text-center text-muted">Нет документов списания</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- СТРОКИ ДОКУМЕНТОВ СПИСАНИЯ (WriteOffDocumentItem) -->
|
||||
<div class="section-card">
|
||||
<h3>📋 Строки документов списания WriteOffDocumentItem ({{ writeoff_document_items.count }})</h3>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-bordered table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Документ</th>
|
||||
<th>Товар</th>
|
||||
<th>Склад</th>
|
||||
<th>Кол-во</th>
|
||||
<th>Причина</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in writeoff_document_items %}
|
||||
<tr>
|
||||
<td>{{ item.id }}</td>
|
||||
<td><strong>{{ item.document.document_number|default:"#" }}{{ item.document.id }}</strong></td>
|
||||
<td><strong>{{ item.product.name }}</strong></td>
|
||||
<td>{{ item.document.warehouse.name }}</td>
|
||||
<td><span class="badge bg-danger">{{ item.quantity }}</span></td>
|
||||
<td>{{ item.reason|default:"-" }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr><td colspan="6" class="text-center text-muted">Нет строк документов списания</td></tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
{% extends 'inventory/base_inventory_minimal.html' %}
|
||||
{% load inventory_filters %}
|
||||
{% load static %}
|
||||
|
||||
{% block inventory_title %}Детали инвентаризации{% endblock %}
|
||||
{% block breadcrumb_current %}Инвентаризация{% endblock %}
|
||||
|
||||
{% block inventory_content %}
|
||||
<!-- CSS для компонента поиска товаров -->
|
||||
<link rel="stylesheet" href="{% static 'products/css/product-search-picker.css' %}">
|
||||
<div class="card">
|
||||
<div class="card-header d-flex justify-content-between align-items-center">
|
||||
<h4 class="mb-0">Инвентаризация: {{ inventory.warehouse.name }}</h4>
|
||||
@@ -18,6 +21,12 @@
|
||||
<div class="col-md-6">
|
||||
<h5>Информация</h5>
|
||||
<table class="table table-borderless">
|
||||
{% if inventory.document_number %}
|
||||
<tr>
|
||||
<th>Номер документа:</th>
|
||||
<td><strong>{{ inventory.document_number }}</strong></td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
<tr>
|
||||
<th>Склад:</th>
|
||||
<td><strong>{{ inventory.warehouse.name }}</strong></td>
|
||||
@@ -26,11 +35,17 @@
|
||||
<th>Статус:</th>
|
||||
<td>
|
||||
{% if inventory.status == 'draft' %}
|
||||
<span class="badge bg-secondary">Черновик</span>
|
||||
<span class="badge bg-secondary fs-6 px-3 py-2">
|
||||
<i class="bi bi-file-earmark"></i> Черновик
|
||||
</span>
|
||||
{% elif inventory.status == 'processing' %}
|
||||
<span class="badge bg-warning">В обработке</span>
|
||||
<span class="badge bg-warning text-dark fs-6 px-3 py-2">
|
||||
<i class="bi bi-hourglass-split"></i> В обработке
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="badge bg-success">Завершена</span>
|
||||
<span class="badge bg-success fs-6 px-3 py-2">
|
||||
<i class="bi bi-check-circle-fill"></i> Завершена
|
||||
</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -48,66 +63,209 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if inventory.status == 'completed' %}
|
||||
<!-- Информация о созданных документах -->
|
||||
<div class="alert alert-info mb-4">
|
||||
<h6><i class="bi bi-info-circle"></i> Инвентаризация завершена</h6>
|
||||
<div class="mt-2">
|
||||
{% if writeoff_document %}
|
||||
<p class="mb-1">
|
||||
<strong>Документ списания:</strong>
|
||||
<a href="{% url 'inventory:writeoff-document-detail' writeoff_document.pk %}" target="_blank">
|
||||
{{ writeoff_document.document_number }}
|
||||
</a>
|
||||
<span class="badge {% if writeoff_document.status == 'confirmed' %}bg-success{% else %}bg-warning{% endif %} ms-2">
|
||||
{{ writeoff_document.get_status_display }}
|
||||
</span>
|
||||
</p>
|
||||
{% endif %}
|
||||
{% if incoming_document %}
|
||||
<p class="mb-1">
|
||||
<strong>Документ оприходования:</strong>
|
||||
<a href="{% url 'inventory:incoming-document-detail' incoming_document.pk %}" target="_blank">
|
||||
{{ incoming_document.document_number }}
|
||||
</a>
|
||||
<span class="badge {% if incoming_document.status == 'confirmed' %}bg-success{% else %}bg-warning{% endif %} ms-2">
|
||||
{{ incoming_document.get_status_display }}
|
||||
</span>
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<hr>
|
||||
|
||||
<h5>Строки инвентаризации</h5>
|
||||
|
||||
{% if lines %}
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Товар</th>
|
||||
<th>В системе</th>
|
||||
<th>По факту</th>
|
||||
<th>Разница</th>
|
||||
<th>Статус</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for line in lines %}
|
||||
<tr>
|
||||
<td>{{ line.product.name }}</td>
|
||||
<td>{{ line.quantity_system }}</td>
|
||||
<td>{{ line.quantity_fact }}</td>
|
||||
<td>
|
||||
<!-- Компонент поиска товаров (только если не завершена) -->
|
||||
{% if inventory.status != 'completed' %}
|
||||
<div class="card border-primary mb-4" id="product-search-section">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="mb-0"><i class="bi bi-plus-square me-2"></i>Добавить товар в инвентаризацию</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% include 'products/components/product_search_picker.html' with container_id='inventory-product-picker' title='Поиск товара для инвентаризации...' warehouse_id=inventory.warehouse.id filter_in_stock_only=False categories=categories tags=tags add_button_text='Добавить товар' content_height='250px' %}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Таблица строк инвентаризации -->
|
||||
<div class="table-responsive">
|
||||
<table class="table table-sm table-hover" id="inventory-lines-table">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Товар</th>
|
||||
<th class="text-center">Всего на складе</th>
|
||||
<th class="text-center">В резервах</th>
|
||||
<th class="bg-light text-center">В системе (свободно)</th>
|
||||
<th class="text-center">Подсчитано (факт, свободные)</th>
|
||||
<th class="text-center">Итоговая разница</th>
|
||||
<th class="text-center">Статус</th>
|
||||
{% if inventory.status != 'completed' %}
|
||||
<th class="text-center">Действия</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="inventory-lines-tbody">
|
||||
{% for line in lines %}
|
||||
<tr data-line-id="{{ line.id }}" class="{% if line.difference != 0 %}table-warning{% endif %}">
|
||||
<td>{{ line.product.name }}</td>
|
||||
<td class="text-center">{{ line.quantity_available|smart_quantity }}</td>
|
||||
<td class="text-center">{{ line.quantity_reserved|smart_quantity }}</td>
|
||||
<td class="bg-light fw-semibold text-center">{{ line.quantity_system|smart_quantity }}</td>
|
||||
<td class="text-center">
|
||||
{% if inventory.status != 'completed' %}
|
||||
<input type="number"
|
||||
class="form-control form-control-sm quantity-fact-input mx-auto"
|
||||
value="{{ line.quantity_fact|stringformat:'g' }}"
|
||||
min="0"
|
||||
step="0.001"
|
||||
data-line-id="{{ line.id }}"
|
||||
style="width: 100px;">
|
||||
{% else %}
|
||||
{{ line.quantity_fact|smart_quantity }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="difference-badge" data-line-id="{{ line.id }}">
|
||||
{% if line.difference > 0 %}
|
||||
<span class="badge bg-success">+{{ line.difference }}</span>
|
||||
<span class="badge bg-success">+{{ line.difference|smart_quantity }}</span>
|
||||
{% elif line.difference < 0 %}
|
||||
<span class="badge bg-danger">{{ line.difference }}</span>
|
||||
<span class="badge bg-danger">{{ line.difference|smart_quantity }}</span>
|
||||
{% else %}
|
||||
<span class="badge bg-secondary">0</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if line.processed %}
|
||||
<span class="badge bg-success">Обработана</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning">Не обработана</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-info">
|
||||
<i class="bi bi-info-circle"></i> Строк инвентаризации не добавлено.
|
||||
<a href="{% url 'inventory:inventory-lines-add' inventory.pk %}" class="alert-link">Добавить строки</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
{% if line.processed %}
|
||||
<span class="badge bg-success">Обработана</span>
|
||||
{% else %}
|
||||
<span class="badge bg-warning">Не проведено</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
{% if inventory.status != 'completed' %}
|
||||
<td class="text-center">
|
||||
<button class="btn btn-sm btn-danger delete-line-btn"
|
||||
data-line-id="{{ line.id }}"
|
||||
data-product-name="{{ line.product.name }}"
|
||||
title="Удалить строку">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr id="empty-lines-message">
|
||||
<td colspan="{% if inventory.status != 'completed' %}8{% else %}7{% endif %}" class="text-center text-muted py-4">
|
||||
<i class="bi bi-info-circle"></i> Строк инвентаризации не добавлено.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2 mt-4">
|
||||
<!-- Кнопки действий -->
|
||||
<div class="d-flex gap-2 mt-4 align-items-center">
|
||||
{% if inventory.status != 'completed' %}
|
||||
<a href="{% url 'inventory:inventory-lines-add' inventory.pk %}" class="btn btn-primary">
|
||||
<i class="bi bi-plus-circle"></i> Добавить строки
|
||||
</a>
|
||||
<!-- Группа кнопок для незавершенной инвентаризации -->
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button" class="btn btn-outline-primary" id="save-inventory-btn" onclick="location.reload();" title="Обновить страницу (изменения сохраняются автоматически)">
|
||||
<i class="bi bi-save"></i> Сохранить
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" id="complete-inventory-btn" {% if not lines %}disabled{% endif %}>
|
||||
<i class="bi bi-check-circle"></i> Завершить инвентаризацию
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="{% url 'inventory:inventory-list' %}" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Вернуться к списку
|
||||
</a>
|
||||
<!-- Кнопка возврата всегда видна -->
|
||||
<div class="ms-auto">
|
||||
<a href="{% url 'inventory:inventory-list' %}" class="btn btn-secondary">
|
||||
<i class="bi bi-arrow-left"></i> Вернуться к списку
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Модальное окно подтверждения завершения инвентаризации -->
|
||||
<div class="modal fade" id="confirmCompleteModal" tabindex="-1" aria-labelledby="confirmCompleteModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header bg-warning">
|
||||
<h5 class="modal-title" id="confirmCompleteModalLabel">
|
||||
<i class="bi bi-exclamation-triangle-fill"></i> Завершить инвентаризацию?
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Вы уверены, что хотите завершить инвентаризацию?</p>
|
||||
<div id="complete-summary" class="alert alert-info">
|
||||
<!-- Заполняется через JavaScript -->
|
||||
</div>
|
||||
<div class="alert alert-warning">
|
||||
<i class="bi bi-exclamation-triangle"></i>
|
||||
<strong>Внимание!</strong> После завершения редактирование строк будет недоступно.
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||||
<form method="post" action="{% url 'inventory:inventory-complete' inventory.pk %}" id="complete-inventory-form">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-warning">Завершить инвентаризацию</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Подключаем JavaScript -->
|
||||
<script src="{% static 'products/js/product-search-picker.js' %}"></script>
|
||||
<script src="{% static 'inventory/js/inventory_detail.js' %}"></script>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Инициализация компонента поиска товаров
|
||||
{% if inventory.status != 'completed' %}
|
||||
const picker = ProductSearchPicker.init('#inventory-product-picker', {
|
||||
onAddSelected: function(product, instance) {
|
||||
if (product) {
|
||||
addInventoryLine(product.id);
|
||||
instance.clearSelection();
|
||||
}
|
||||
}
|
||||
});
|
||||
{% endif %}
|
||||
|
||||
// Инициализация обработчиков
|
||||
const inventoryId = {{ inventory.pk }};
|
||||
window.inventoryDetailHandlers = initInventoryDetailHandlers(inventoryId, {
|
||||
addLineUrl: '{% url "inventory:inventory-line-add" inventory.pk %}',
|
||||
updateLineUrl: '{% url "inventory:inventory-line-update" inventory.pk 999 %}',
|
||||
deleteLineUrl: '{% url "inventory:inventory-line-delete" inventory.pk 999 %}',
|
||||
completeUrl: '{% url "inventory:inventory-complete" inventory.pk %}'
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
<table class="table table-sm table-hover minimal-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Номер</th>
|
||||
<th>Склад</th>
|
||||
<th>Статус</th>
|
||||
<th>Провёл</th>
|
||||
@@ -57,6 +58,7 @@
|
||||
<tbody>
|
||||
{% for inventory in inventories %}
|
||||
<tr>
|
||||
<td class="fw-medium">{{ inventory.document_number|default:"—" }}</td>
|
||||
<td class="fw-medium">{{ inventory.warehouse.name }}</td>
|
||||
<td>
|
||||
<span class="status-dot status-{{ inventory.status }}"></span>
|
||||
@@ -69,8 +71,8 @@
|
||||
<td class="text-muted">{{ inventory.conducted_by|default:"—" }}</td>
|
||||
<td class="text-muted">{{ inventory.date|date:"d.m.Y H:i" }}</td>
|
||||
<td class="text-end">
|
||||
<a href="{% url 'inventory:inventory-detail' inventory.pk %}" class="btn-icon" title="Просмотр">
|
||||
<i class="bi bi-eye"></i>
|
||||
<a href="{% url 'inventory:inventory-detail' inventory.pk %}" class="btn-icon" title="Открыть для редактирования">
|
||||
<i class="bi bi-pencil-square"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -114,6 +116,13 @@
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Подсказка для пользователя -->
|
||||
<div class="alert alert-info mt-3" style="font-size: 0.875rem;">
|
||||
<i class="bi bi-info-circle"></i>
|
||||
<strong>Как использовать:</strong> Создайте инвентаризацию, затем откройте её (иконка карандаша) для добавления товаров и указания фактических количеств.
|
||||
После завершения будут автоматически созданы документы списания и оприходования (черновики), которые можно провести отдельно.
|
||||
</div>
|
||||
|
||||
<style>
|
||||
/* Минималистичная таблица */
|
||||
.minimal-table {
|
||||
|
||||
Reference in New Issue
Block a user