""" Сервис для работы с документами поступления (IncomingDocument). Обеспечивает: - Создание документов с автонумерацией - Добавление позиций в черновик - Проведение документов (создание IncomingBatch и Incoming) - Отмену документов """ from decimal import Decimal from django.db import transaction from django.utils import timezone from django.core.exceptions import ValidationError from inventory.models import ( IncomingDocument, IncomingDocumentItem, IncomingBatch, Incoming ) from inventory.utils.document_generator import generate_incoming_document_number class IncomingDocumentService: """ Сервис для работы с документами поступления. """ @classmethod @transaction.atomic def create_document(cls, warehouse, date, receipt_type='supplier', supplier_name=None, notes=None, created_by=None): """ Создать новый документ поступления (черновик). Args: warehouse: объект Warehouse date: дата документа (date) receipt_type: тип поступления ('supplier', 'inventory', 'adjustment') supplier_name: наименование поставщика (str, опционально, для типа 'supplier') notes: примечания (str, опционально) created_by: пользователь (User, опционально) Returns: IncomingDocument """ document = IncomingDocument.objects.create( document_number=generate_incoming_document_number(), warehouse=warehouse, status='draft', date=date, receipt_type=receipt_type, supplier_name=supplier_name if receipt_type == 'supplier' else None, notes=notes, created_by=created_by ) return document @classmethod @transaction.atomic def add_item(cls, document, product, quantity, cost_price, notes=None): """ Добавить позицию в документ поступления. Args: document: IncomingDocument product: Product quantity: Decimal - количество товара cost_price: Decimal - закупочная цена notes: str - примечания Returns: IncomingDocumentItem Raises: ValidationError: если документ не черновик """ if document.status != 'draft': raise ValidationError( "Нельзя добавлять позиции в проведённый или отменённый документ" ) quantity = Decimal(str(quantity)) if quantity <= 0: raise ValidationError("Количество должно быть больше нуля") cost_price = Decimal(str(cost_price)) if cost_price < 0: raise ValidationError("Закупочная цена не может быть отрицательной") # Создаем позицию документа item = IncomingDocumentItem.objects.create( document=document, product=product, quantity=quantity, cost_price=cost_price, notes=notes ) return item @classmethod @transaction.atomic def update_item(cls, item, quantity=None, cost_price=None, notes=None): """ Обновить позицию документа. Args: item: IncomingDocumentItem quantity: новое количество (опционально) cost_price: новая закупочная цена (опционально) notes: новые примечания (опционально) Returns: IncomingDocumentItem Raises: ValidationError: если документ не черновик """ if item.document.status != 'draft': raise ValidationError( "Нельзя редактировать позиции проведённого или отменённого документа" ) if quantity is not None: quantity = Decimal(str(quantity)) if quantity <= 0: raise ValidationError("Количество должно быть больше нуля") item.quantity = quantity if cost_price is not None: cost_price = Decimal(str(cost_price)) if cost_price < 0: raise ValidationError("Закупочная цена не может быть отрицательной") item.cost_price = cost_price if notes is not None: item.notes = notes item.save() return item @classmethod @transaction.atomic def remove_item(cls, item): """ Удалить позицию из документа. Args: item: IncomingDocumentItem Raises: ValidationError: если документ не черновик """ if item.document.status != 'draft': raise ValidationError( "Нельзя удалять позиции из проведённого или отменённого документа" ) # Удаляем позицию item.delete() @classmethod @transaction.atomic def confirm_document(cls, document, confirmed_by=None): """ Провести документ поступления. Процесс: 1. Проверяем что документ - черновик и имеет позиции 2. Создаем IncomingBatch с номером документа 3. Для каждой позиции создаем Incoming запись 4. Сигнал create_stock_batch_on_incoming автоматически создаст StockBatch 5. Меняем статус документа на 'confirmed' Args: document: IncomingDocument confirmed_by: User - кто проводит документ Returns: dict: результат проведения Raises: ValidationError: если документ нельзя провести """ if document.status != 'draft': raise ValidationError( f"Документ уже проведён или отменён (статус: {document.get_status_display()})" ) if not document.items.exists(): raise ValidationError("Нельзя провести пустой документ") # Создаем IncomingBatch incoming_batch = IncomingBatch.objects.create( warehouse=document.warehouse, document_number=document.document_number, receipt_type=document.receipt_type, supplier_name=document.supplier_name if document.receipt_type == 'supplier' else '', notes=document.notes ) # Создаем Incoming записи для каждого товара incomings_created = [] total_cost = Decimal('0') for item in document.items.select_related('product'): incoming = Incoming.objects.create( batch=incoming_batch, product=item.product, quantity=item.quantity, cost_price=item.cost_price, notes=item.notes ) incomings_created.append(incoming) total_cost += item.total_cost # Обновляем статус документа document.status = 'confirmed' document.confirmed_by = confirmed_by document.confirmed_at = timezone.now() document.save(update_fields=['status', 'confirmed_by', 'confirmed_at', 'updated_at']) return { 'document': document, 'incoming_batch': incoming_batch, 'incomings_created': len(incomings_created), 'total_quantity': document.total_quantity, 'total_cost': total_cost } @classmethod @transaction.atomic def cancel_document(cls, document): """ Отменить документ поступления (черновик). Args: document: IncomingDocument Returns: IncomingDocument Raises: ValidationError: если документ уже проведён """ if document.status == 'confirmed': raise ValidationError( "Нельзя отменить проведённый документ. " "Создайте новый документ для корректировки." ) if document.status == 'cancelled': raise ValidationError("Документ уже отменён") # Обновляем статус документа document.status = 'cancelled' document.save(update_fields=['status', 'updated_at']) return document @staticmethod def get_draft_documents(warehouse=None): """ Получить все черновики документов поступления. Args: warehouse: фильтр по складу (опционально) Returns: QuerySet[IncomingDocument] """ qs = IncomingDocument.objects.filter(status='draft') if warehouse: qs = qs.filter(warehouse=warehouse) return qs.select_related('warehouse', 'created_by').prefetch_related('items') @staticmethod def get_today_drafts(warehouse): """ Получить черновики за сегодня для склада. Args: warehouse: Warehouse Returns: QuerySet[IncomingDocument] """ today = timezone.now().date() return IncomingDocument.objects.filter( warehouse=warehouse, status='draft', date=today ).select_related('warehouse', 'created_by')