Улучшения инвентаризации: автоматическое проведение документов, оптимизация запросов и улучшения 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:
2025-12-21 23:59:02 +03:00
parent bb821f9ef4
commit a8ba5ce780
16 changed files with 1619 additions and 194 deletions

View File

@@ -6,7 +6,7 @@ from decimal import Decimal
from django.test import TestCase
from products.models import Product
from inventory.models import Warehouse, StockBatch, Incoming, Sale, WriteOff, Transfer, Inventory, InventoryLine, Reservation, Stock
from inventory.models import Warehouse, StockBatch, Sale, Transfer, Inventory, InventoryLine, Reservation, Stock
from inventory.services import StockBatchManager, SaleProcessor, InventoryProcessor
from orders.models import Order, OrderItem
from customers.models import Customer
@@ -302,17 +302,23 @@ class InventoryProcessorTest(TestCase):
# Проверяем результат
self.assertEqual(result['processed_lines'], 1)
self.assertEqual(result['writeoffs_created'], 1)
self.assertEqual(result['incomings_created'], 0)
self.assertIsNotNone(result['writeoff_document'])
self.assertIsNone(result['incoming_document'])
# Проверяем, что создалось списание
writeoffs = WriteOff.objects.filter(batch=batch)
self.assertEqual(writeoffs.count(), 1)
self.assertEqual(writeoffs.first().quantity, Decimal('15'))
# Проверяем остаток в партии
# Проверяем, что создался документ списания (черновик)
writeoff_doc = result['writeoff_document']
self.assertEqual(writeoff_doc.status, 'draft')
self.assertEqual(writeoff_doc.inventory, inventory)
# Проверяем, что в документе есть позиция
items = writeoff_doc.items.all()
self.assertEqual(items.count(), 1)
self.assertEqual(items.first().product, self.product)
self.assertEqual(items.first().quantity, Decimal('15'))
# Проверяем, что документ еще не проведен - остаток не изменился
batch.refresh_from_db()
self.assertEqual(batch.quantity, Decimal('85'))
self.assertEqual(batch.quantity, Decimal('100')) # Остаток не изменился, т.к. документ не проведен
def test_process_inventory_surplus(self):
"""Тест обработки излишка при инвентаризации."""
@@ -341,13 +347,25 @@ class InventoryProcessorTest(TestCase):
# Проверяем результат
self.assertEqual(result['processed_lines'], 1)
self.assertEqual(result['writeoffs_created'], 0)
self.assertEqual(result['incomings_created'], 1)
self.assertIsNone(result['writeoff_document'])
self.assertIsNotNone(result['incoming_document'])
# Проверяем, что создалось приходование
incomings = Incoming.objects.filter(product=self.product)
self.assertEqual(incomings.count(), 1)
self.assertEqual(incomings.first().quantity, Decimal('20'))
# Проверяем, что создался документ оприходования (черновик)
incoming_doc = result['incoming_document']
self.assertEqual(incoming_doc.status, 'draft')
self.assertEqual(incoming_doc.inventory, inventory)
self.assertEqual(incoming_doc.receipt_type, 'inventory')
# Проверяем, что в документе есть позиция
items = incoming_doc.items.all()
self.assertEqual(items.count(), 1)
self.assertEqual(items.first().product, self.product)
self.assertEqual(items.first().quantity, Decimal('20'))
# Проверяем, что документ еще не проведен - новый StockBatch не создан
from inventory.models import StockBatch
batches = StockBatch.objects.filter(product=self.product, warehouse=self.warehouse)
self.assertEqual(batches.count(), 1) # Только исходная партия, новая не создана
class ReservationSignalsTest(TestCase):