""" Сервис для работы с трансформациями товаров (Transformation). Обеспечивает: - Создание документов трансформации с автонумерацией - Добавление входных/выходных товаров с автоматическим резервированием - Проведение трансформации (FIFO списание + оприходование) - Отмену трансформации (откат операций) """ from decimal import Decimal from django.db import transaction from django.utils import timezone from django.core.exceptions import ValidationError from inventory.models import ( Transformation, TransformationInput, TransformationOutput, Reservation, Stock, StockBatch, DocumentCounter ) from inventory.services.batch_manager import StockBatchManager class TransformationService: """ Сервис для работы с трансформациями товаров. """ @classmethod def generate_document_number(cls): """Генерация номера документа трансформации""" next_num = DocumentCounter.get_next_value('transformation') return f"TR-{next_num:05d}" @classmethod @transaction.atomic def create_transformation(cls, warehouse, comment=None, employee=None): """ Создать новый документ трансформации (черновик). Args: warehouse: объект Warehouse comment: комментарий (str, опционально) employee: сотрудник (User, опционально) Returns: Transformation """ transformation = Transformation.objects.create( document_number=cls.generate_document_number(), warehouse=warehouse, status='draft', comment=comment or '', employee=employee ) return transformation @classmethod @transaction.atomic def add_input(cls, transformation, product, quantity): """ Добавить входной товар в трансформацию. Автоматически создает резерв (через сигнал). Args: transformation: Transformation product: Product quantity: Decimal - количество для списания Returns: TransformationInput Raises: ValidationError: если трансформация не черновик или недостаточно товара """ if transformation.status != 'draft': raise ValidationError( "Нельзя добавлять позиции в проведённую или отменённую трансформацию" ) quantity = Decimal(str(quantity)) if quantity <= 0: raise ValidationError("Количество должно быть больше нуля") # Проверяем что товар еще не добавлен if transformation.inputs.filter(product=product).exists(): raise ValidationError( f"Товар '{product.name}' уже добавлен в качестве входного" ) # Проверяем доступное количество stock = Stock.objects.filter( product=product, warehouse=transformation.warehouse ).first() if not stock: raise ValidationError( f"Товар '{product.name}' отсутствует на складе '{transformation.warehouse.name}'" ) # quantity_free = quantity_available - quantity_reserved available = stock.quantity_available - stock.quantity_reserved if quantity > available: raise ValidationError( f"Недостаточно свободного товара '{product.name}'. " f"Доступно: {available}, запрашивается: {quantity}" ) # Создаем входной товар (резерв создастся автоматически через сигнал) trans_input = TransformationInput.objects.create( transformation=transformation, product=product, quantity=quantity ) return trans_input @classmethod @transaction.atomic def add_output(cls, transformation, product, quantity): """ Добавить выходной товар в трансформацию. Args: transformation: Transformation product: Product quantity: Decimal - количество получаемого товара Returns: TransformationOutput Raises: ValidationError: если трансформация не черновик """ if transformation.status != 'draft': raise ValidationError( "Нельзя добавлять позиции в проведённую или отменённую трансформацию" ) quantity = Decimal(str(quantity)) if quantity <= 0: raise ValidationError("Количество должно быть больше нуля") # Проверяем что товар еще не добавлен if transformation.outputs.filter(product=product).exists(): raise ValidationError( f"Товар '{product.name}' уже добавлен в качестве выходного" ) # Создаем выходной товар trans_output = TransformationOutput.objects.create( transformation=transformation, product=product, quantity=quantity ) return trans_output @classmethod @transaction.atomic def remove_input(cls, trans_input): """ Удалить входной товар из трансформации. Автоматически освобождает резерв (через сигнал). Args: trans_input: TransformationInput Raises: ValidationError: если трансформация не черновик """ if trans_input.transformation.status != 'draft': raise ValidationError( "Нельзя удалять позиции из проведённой или отменённой трансформации" ) trans_input.delete() @classmethod @transaction.atomic def remove_output(cls, trans_output): """ Удалить выходной товар из трансформации. Args: trans_output: TransformationOutput Raises: ValidationError: если трансформация не черновик """ if trans_output.transformation.status != 'draft': raise ValidationError( "Нельзя удалять позиции из проведённой или отменённой трансформации" ) trans_output.delete() @classmethod @transaction.atomic def confirm(cls, transformation): """ Провести трансформацию (completed). FIFO списание входных товаров и оприходование выходных. Выполняется через сигнал process_transformation_on_complete. Args: transformation: Transformation Raises: ValidationError: если документ не готов к проведению """ if transformation.status != 'draft': raise ValidationError("Документ уже проведён или отменён") if not transformation.inputs.exists(): raise ValidationError("Добавьте хотя бы один входной товар") if not transformation.outputs.exists(): raise ValidationError("Добавьте хотя бы один выходной товар") # Проверяем наличие всех входных товаров for trans_input in transformation.inputs.all(): stock = Stock.objects.filter( product=trans_input.product, warehouse=transformation.warehouse ).first() if not stock: raise ValidationError( f"Товар '{trans_input.product.name}' отсутствует на складе" ) # Учитываем резервы (включая резерв этой трансформации) reserved_qty = Reservation.objects.filter( product=trans_input.product, warehouse=transformation.warehouse, status='reserved' ).exclude( transformation_input__transformation=transformation ).aggregate(total=models.Sum('quantity'))['total'] or Decimal('0') available = stock.quantity_available - reserved_qty if trans_input.quantity > available: raise ValidationError( f"Недостаточно свободного товара '{trans_input.product.name}'. " f"Доступно: {available}, требуется: {trans_input.quantity}" ) # Меняем статус (списание и оприходование происходит через сигнал) transformation.status = 'completed' transformation.save(update_fields=['status', 'updated_at']) return transformation @classmethod @transaction.atomic def cancel(cls, transformation): """ Отменить трансформацию. Откатывает операции если была проведена (через сигнал). Args: transformation: Transformation Raises: ValidationError: если уже отменена """ if transformation.status == 'cancelled': raise ValidationError("Трансформация уже отменена") # Меняем статус (откат через сигнал если было completed) transformation.status = 'cancelled' transformation.save(update_fields=['status', 'updated_at']) return transformation # Импорт models для использования в методе confirm from django.db import models