""" Процессор для обработки инвентаризации. Основной функционал: - Обработка расхождений между фактом и системой - Автоматическое создание WriteOff для недостач (по FIFO) - Автоматическое создание Incoming для излишков """ from decimal import Decimal from django.db import transaction from django.utils import timezone from inventory.models import ( Inventory, InventoryLine, WriteOff, Incoming, StockBatch, Stock ) from inventory.services.batch_manager import StockBatchManager class InventoryProcessor: """ Обработчик инвентаризации с автоматической коррекцией остатков. """ @staticmethod @transaction.atomic def process_inventory(inventory_id): """ Обработать инвентаризацию: - Для недостач (разница < 0): создать WriteOff по FIFO - Для излишков (разница > 0): создать Incoming с новой партией - Обновить статус inventory и lines Args: inventory_id: ID объекта Inventory Returns: dict: { 'inventory': Inventory, 'processed_lines': int, 'writeoffs_created': int, 'incomings_created': int, 'errors': [...] } """ inventory = Inventory.objects.get(id=inventory_id) lines = InventoryLine.objects.filter(inventory=inventory, processed=False) writeoffs_created = 0 incomings_created = 0 errors = [] try: for line in lines: try: if line.difference < 0: # Недостача: списать по FIFO InventoryProcessor._create_writeoff_for_deficit( inventory, line ) writeoffs_created += 1 elif line.difference > 0: # Излишек: создать новую партию InventoryProcessor._create_incoming_for_surplus( inventory, line ) incomings_created += 1 # Отмечаем строку как обработанную line.processed = True line.save(update_fields=['processed']) except Exception as e: errors.append({ 'line': line, 'error': str(e) }) # Обновляем статус инвентаризации inventory.status = 'completed' inventory.save(update_fields=['status']) except Exception as e: errors.append({ 'inventory': inventory, 'error': str(e) }) return { 'inventory': inventory, 'processed_lines': lines.count(), 'writeoffs_created': writeoffs_created, 'incomings_created': incomings_created, 'errors': errors } @staticmethod def _create_writeoff_for_deficit(inventory, line): """ Создать операцию WriteOff для недостачи при инвентаризации. Списывается по FIFO из старейших партий. Args: inventory: объект Inventory line: объект InventoryLine с negative difference """ quantity_to_writeoff = abs(line.difference) # Списываем по FIFO allocations = StockBatchManager.write_off_by_fifo( line.product, inventory.warehouse, quantity_to_writeoff ) # Создаем WriteOff для каждой партии for batch, qty_allocated in allocations: WriteOff.objects.create( batch=batch, quantity=qty_allocated, reason='inventory', cost_price=batch.cost_price, notes=f'Инвентаризация {inventory.id}, строка {line.id}' ) @staticmethod def _create_incoming_for_surplus(inventory, line): """ Создать операцию Incoming для излишка при инвентаризации. Новая партия создается с последней известной cost_price товара. Args: inventory: объект Inventory line: объект InventoryLine с positive difference """ quantity_surplus = line.difference # Получаем последнюю known cost_price cost_price = InventoryProcessor._get_last_cost_price( line.product, inventory.warehouse ) # Создаем новую партию batch = StockBatchManager.create_batch( line.product, inventory.warehouse, quantity_surplus, cost_price ) # Создаем документ Incoming Incoming.objects.create( product=line.product, warehouse=inventory.warehouse, quantity=quantity_surplus, cost_price=cost_price, batch=batch, notes=f'Инвентаризация {inventory.id}, строка {line.id}' ) @staticmethod def _get_last_cost_price(product, warehouse): """ Получить последнюю известную закупочную цену товара на складе. Используется для создания новой партии при излишке. Порядок поиска: 1. Последняя активная партия на этом складе 2. Последняя активная партия на любом складе 3. cost_price из карточки Product (если есть) 4. Дефолт 0 (если ничего не найдено) Args: product: объект Product warehouse: объект Warehouse Returns: Decimal - закупочная цена """ from inventory.models import StockBatch # Вариант 1: последняя партия на этом складе last_batch = StockBatch.objects.filter( product=product, warehouse=warehouse, is_active=True ).order_by('-created_at').first() if last_batch: return last_batch.cost_price # Вариант 2: последняя партия на любом складе last_batch_any = StockBatch.objects.filter( product=product, is_active=True ).order_by('-created_at').first() if last_batch_any: return last_batch_any.cost_price # Вариант 3: cost_price из карточки товара if product.cost_price: return product.cost_price # Вариант 4: ноль (не должно быть) return Decimal('0') @staticmethod def create_inventory_lines_from_current_stock(inventory): """ Автоматически создать InventoryLine для всех товаров на складе. Используется для удобства: оператор может сразу начать вводить фактические количества, имея под рукой системные остатки. Args: inventory: объект Inventory """ from inventory.models import StockBatch # Получаем все товары, которые есть на этом складе batches = StockBatch.objects.filter( warehouse=inventory.warehouse, is_active=True ).values('product').distinct() for batch_dict in batches: product = batch_dict['product'] # Рассчитываем системный остаток quantity_system = StockBatchManager.get_total_stock(product, inventory.warehouse) # Создаем строку инвентаризации (факт будет заполнен оператором) InventoryLine.objects.get_or_create( inventory=inventory, product_id=product, defaults={ 'quantity_system': quantity_system, 'quantity_fact': 0, # Оператор должен заполнить } ) @staticmethod def get_inventory_report(inventory): """ Получить отчет по инвентаризации. Args: inventory: объект Inventory Returns: dict: { 'inventory': Inventory, 'total_lines': int, 'total_deficit': Decimal, 'total_surplus': Decimal, 'lines': [...] } """ lines = InventoryLine.objects.filter(inventory=inventory).select_related('product') total_deficit = Decimal('0') total_surplus = Decimal('0') lines_data = [] for line in lines: if line.difference < 0: total_deficit += abs(line.difference) elif line.difference > 0: total_surplus += line.difference lines_data.append({ 'line': line, 'system_value': line.quantity_system * line.product.cost_price, 'fact_value': line.quantity_fact * line.product.cost_price, 'value_difference': (line.quantity_fact - line.quantity_system) * line.product.cost_price, }) return { 'inventory': inventory, 'total_lines': lines.count(), 'total_deficit': total_deficit, 'total_surplus': total_surplus, 'lines': lines_data }