""" Тесты для складского учета с FIFO логикой. """ from decimal import Decimal from django.test import TestCase from products.models import Product 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 class WarehouseModelTest(TestCase): """Тесты модели Warehouse.""" def setUp(self): self.warehouse = Warehouse.objects.create( name='Основной склад', description='Главный склад компании' ) def test_warehouse_creation(self): """Тест создания склада.""" self.assertEqual(self.warehouse.name, 'Основной склад') self.assertTrue(self.warehouse.is_active) self.assertIsNotNone(self.warehouse.created_at) def test_warehouse_str(self): """Тест строкового представления склада.""" self.assertEqual(str(self.warehouse), 'Основной склад') class StockBatchManagerFIFOTest(TestCase): """Тесты FIFO логики для партий товаров.""" def setUp(self): """Подготовка тестовых данных.""" # Создаем склад self.warehouse = Warehouse.objects.create(name='Склад 1') # Создаем товар self.product = Product.objects.create( name='Роза красная', cost_price=Decimal('10.00'), sale_price=Decimal('30.00') ) def test_create_batch(self): """Тест создания новой партии.""" batch = StockBatchManager.create_batch( product=self.product, warehouse=self.warehouse, quantity=Decimal('100'), cost_price=Decimal('10.00') ) self.assertEqual(batch.quantity, Decimal('100')) self.assertEqual(batch.cost_price, Decimal('10.00')) self.assertTrue(batch.is_active) def test_fifo_write_off_single_batch(self): """Тест FIFO списания из одной партии.""" # Создаем партию batch = StockBatchManager.create_batch( product=self.product, warehouse=self.warehouse, quantity=Decimal('100'), cost_price=Decimal('10.00') ) # Списываем 50 шт allocations = StockBatchManager.write_off_by_fifo( product=self.product, warehouse=self.warehouse, quantity_to_write_off=Decimal('50') ) # Проверяем результат self.assertEqual(len(allocations), 1) self.assertEqual(allocations[0][1], Decimal('50')) # qty_written # Проверяем остаток в партии batch.refresh_from_db() self.assertEqual(batch.quantity, Decimal('50')) self.assertTrue(batch.is_active) def test_fifo_write_off_multiple_batches(self): """Тест FIFO списания из нескольких партий (старые первыми).""" # Создаем 3 партии в разные моменты batch1 = StockBatchManager.create_batch( product=self.product, warehouse=self.warehouse, quantity=Decimal('30'), cost_price=Decimal('10.00') # Старейшая ) batch2 = StockBatchManager.create_batch( product=self.product, warehouse=self.warehouse, quantity=Decimal('40'), cost_price=Decimal('12.00') ) batch3 = StockBatchManager.create_batch( product=self.product, warehouse=self.warehouse, quantity=Decimal('50'), cost_price=Decimal('15.00') # Новейшая ) # Списываем 100 шт (должно быть: вся batch1, вся batch2, 30 из batch3) allocations = StockBatchManager.write_off_by_fifo( product=self.product, warehouse=self.warehouse, quantity_to_write_off=Decimal('100') ) # Проверяем FIFO порядок self.assertEqual(len(allocations), 3) self.assertEqual(allocations[0][0].id, batch1.id) # Первая списана batch1 self.assertEqual(allocations[0][1], Decimal('30')) # Всё из batch1 self.assertEqual(allocations[1][0].id, batch2.id) # Вторая списана batch2 self.assertEqual(allocations[1][1], Decimal('40')) # Всё из batch2 self.assertEqual(allocations[2][0].id, batch3.id) # Третья batch3 self.assertEqual(allocations[2][1], Decimal('30')) # 30 из batch3 # Проверяем остатки batch1.refresh_from_db() batch2.refresh_from_db() batch3.refresh_from_db() self.assertEqual(batch1.quantity, Decimal('0')) self.assertFalse(batch1.is_active) # Деактивирована self.assertEqual(batch2.quantity, Decimal('0')) self.assertFalse(batch2.is_active) self.assertEqual(batch3.quantity, Decimal('20')) self.assertTrue(batch3.is_active) def test_insufficient_stock_error(self): """Тест ошибки при недостаточном товаре на складе.""" batch = StockBatchManager.create_batch( product=self.product, warehouse=self.warehouse, quantity=Decimal('50'), cost_price=Decimal('10.00') ) # Пытаемся списать больше, чем есть with self.assertRaises(ValueError) as context: StockBatchManager.write_off_by_fifo( product=self.product, warehouse=self.warehouse, quantity_to_write_off=Decimal('100') ) self.assertIn('Недостаточно товара', str(context.exception)) def test_transfer_batch(self): """Тест перемещения товара между складами с сохранением цены.""" warehouse2 = Warehouse.objects.create(name='Склад 2') # Создаем партию на первом складе batch1 = StockBatchManager.create_batch( product=self.product, warehouse=self.warehouse, quantity=Decimal('100'), cost_price=Decimal('10.00') ) # Переносим 40 шт на второй склад new_batch = StockBatchManager.transfer_batch( batch=batch1, to_warehouse=warehouse2, quantity=Decimal('40') ) # Проверяем результаты batch1.refresh_from_db() self.assertEqual(batch1.quantity, Decimal('60')) self.assertEqual(new_batch.warehouse, warehouse2) self.assertEqual(new_batch.quantity, Decimal('40')) self.assertEqual(new_batch.cost_price, Decimal('10.00')) # Цена сохранена! class SaleProcessorTest(TestCase): """Тесты обработки продаж с FIFO списанием.""" def setUp(self): self.warehouse = Warehouse.objects.create(name='Склад 1') self.product = Product.objects.create( name='Гвоздика', cost_price=Decimal('5.00'), sale_price=Decimal('20.00') ) def test_create_sale_with_fifo(self): """Тест создания продажи с FIFO списанием.""" # Создаем партии batch1 = StockBatchManager.create_batch( self.product, self.warehouse, Decimal('30'), Decimal('5.00') ) batch2 = StockBatchManager.create_batch( self.product, self.warehouse, Decimal('50'), Decimal('6.00') ) # Создаем продажу 40 шт sale = SaleProcessor.create_sale( product=self.product, warehouse=self.warehouse, quantity=Decimal('40'), sale_price=Decimal('20.00') ) # Проверяем Sale self.assertTrue(sale.processed) self.assertEqual(sale.quantity, Decimal('40')) # Проверяем FIFO распределение allocations = list(sale.batch_allocations.all()) self.assertEqual(len(allocations), 2) self.assertEqual(allocations[0].quantity, Decimal('30')) # Всё из batch1 self.assertEqual(allocations[1].quantity, Decimal('10')) # 10 из batch2 def test_sale_cost_analysis(self): """Тест анализа себестоимости продажи.""" # Создаем партии с разными ценами batch1 = StockBatchManager.create_batch( self.product, self.warehouse, Decimal('30'), Decimal('5.00') ) batch2 = StockBatchManager.create_batch( self.product, self.warehouse, Decimal('50'), Decimal('10.00') ) # Создаем продажу sale = SaleProcessor.create_sale( product=self.product, warehouse=self.warehouse, quantity=Decimal('40'), sale_price=Decimal('25.00') ) # Анализируем прибыль analysis = SaleProcessor.get_sale_cost_analysis(sale) # Проверяем финансы # batch1: 30 * 5 = 150 себестоимость, 30 * 25 = 750 выручка # batch2: 10 * 10 = 100 себестоимость, 10 * 25 = 250 выручка # Итого: 250 себестоимость, 1000 выручка, 750 прибыль self.assertEqual(analysis['total_cost'], Decimal('250')) self.assertEqual(analysis['total_revenue'], Decimal('1000')) self.assertEqual(analysis['total_profit'], Decimal('750')) self.assertEqual(analysis['profit_margin'], Decimal('75.00')) # 750/1000*100 class InventoryProcessorTest(TestCase): """Тесты обработки инвентаризации.""" def setUp(self): self.warehouse = Warehouse.objects.create(name='Склад 1') self.product = Product.objects.create( name='Тюльпан', cost_price=Decimal('8.00'), sale_price=Decimal('25.00') ) def test_process_inventory_deficit(self): """Тест обработки недостачи при инвентаризации.""" # Создаем партию batch = StockBatchManager.create_batch( self.product, self.warehouse, Decimal('100'), Decimal('8.00') ) # Создаем инвентаризацию inventory = Inventory.objects.create( warehouse=self.warehouse, status='draft' ) # Строка: в системе 100, по факту 85 (недостача 15) line = InventoryLine.objects.create( inventory=inventory, product=self.product, quantity_system=Decimal('100'), quantity_fact=Decimal('85') ) # Обрабатываем инвентаризацию result = InventoryProcessor.process_inventory(inventory.id) # Проверяем результат self.assertEqual(result['processed_lines'], 1) self.assertIsNotNone(result['writeoff_document']) self.assertIsNone(result['incoming_document']) # Проверяем, что создался документ списания (черновик) 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('100')) # Остаток не изменился, т.к. документ не проведен def test_process_inventory_surplus(self): """Тест обработки излишка при инвентаризации.""" # Создаем партию batch = StockBatchManager.create_batch( self.product, self.warehouse, Decimal('100'), Decimal('8.00') ) # Создаем инвентаризацию inventory = Inventory.objects.create( warehouse=self.warehouse, status='draft' ) # Строка: в системе 100, по факту 120 (излишек 20) line = InventoryLine.objects.create( inventory=inventory, product=self.product, quantity_system=Decimal('100'), quantity_fact=Decimal('120') ) # Обрабатываем инвентаризацию result = InventoryProcessor.process_inventory(inventory.id) # Проверяем результат self.assertEqual(result['processed_lines'], 1) self.assertIsNone(result['writeoff_document']) self.assertIsNotNone(result['incoming_document']) # Проверяем, что создался документ оприходования (черновик) 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): """Тесты автоматического резервирования через сигналы.""" def setUp(self): self.warehouse = Warehouse.objects.create(name='Склад 1') self.product = Product.objects.create( name='Нарцисс', cost_price=Decimal('6.00'), sale_price=Decimal('18.00') ) self.customer = Customer.objects.create( name='Иван Иванов', phone='+375291234567' ) def test_reservation_on_order_create(self): """Тест создания резервирования при создании заказа.""" # Создаем заказ order = Order.objects.create( customer=self.customer, order_number='ORD-20250101-0001', delivery_type='courier' ) # Добавляем товар в заказ item = OrderItem.objects.create( order=order, product=self.product, quantity=5, price=Decimal('18.00') ) # Проверяем, что резерв создан reservations = Reservation.objects.filter(order_item=item) self.assertEqual(reservations.count(), 1) res = reservations.first() self.assertEqual(res.quantity, Decimal('5')) self.assertEqual(res.status, 'reserved') def test_release_reservation_on_order_delete(self): """Тест освобождения резервирования при удалении заказа.""" # Создаем заказ с товаром order = Order.objects.create( customer=self.customer, order_number='ORD-20250101-0002', delivery_type='courier' ) item = OrderItem.objects.create( order=order, product=self.product, quantity=10, price=Decimal('18.00') ) # Проверяем, что резерв создан res = Reservation.objects.get(order_item=item) self.assertEqual(res.status, 'reserved') # Удаляем заказ order.delete() # Проверяем, что резерв освобожден res.refresh_from_db() self.assertEqual(res.status, 'released') self.assertIsNotNone(res.released_at) class StockCacheTest(TestCase): """Тесты кеширования остатков в модели Stock.""" def setUp(self): self.warehouse = Warehouse.objects.create(name='Склад 1') self.product = Product.objects.create( name='Лилия', cost_price=Decimal('12.00'), sale_price=Decimal('40.00') ) def test_stock_refresh_from_batches(self): """Тест пересчета остатков из партий.""" # Создаем партии batch1 = StockBatchManager.create_batch( self.product, self.warehouse, Decimal('50'), Decimal('12.00') ) batch2 = StockBatchManager.create_batch( self.product, self.warehouse, Decimal('75'), Decimal('13.00') ) # Получаем или создаем Stock stock, created = Stock.objects.get_or_create( product=self.product, warehouse=self.warehouse ) # Обновляем из батчей stock.refresh_from_batches() # Проверяем результат self.assertEqual(stock.quantity_available, Decimal('125')) def test_stock_quantity_free(self): """Тест расчета свободного количества.""" batch = StockBatchManager.create_batch( self.product, self.warehouse, Decimal('100'), Decimal('12.00') ) # Создаем резерв Reservation.objects.create( product=self.product, warehouse=self.warehouse, quantity=Decimal('30'), status='reserved' ) # Получаем Stock и обновляем stock, created = Stock.objects.get_or_create( product=self.product, warehouse=self.warehouse ) stock.refresh_from_batches() # Проверяем: доступно 100, зарезервировано 30, свободно 70 self.assertEqual(stock.quantity_available, Decimal('100')) self.assertEqual(stock.quantity_reserved, Decimal('30')) self.assertEqual(stock.quantity_free, Decimal('70'))