Files
octopus/myproject/inventory/services/incoming_document_service.py
Andrey Smakotin c534e27c41 refactor: подготовка к стандартизации Transfer моделей
Текущее состояние перед рефакторингом Transfer → TransferDocument.
Все изменения с последнего коммита по улучшению системы поступлений.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-26 19:55:50 +03:00

292 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Сервис для работы с документами поступления (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, StockBatch, Stock
)
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. Для каждой позиции создаем StockBatch напрямую
3. Обновляем Stock
4. Меняем статус документа на '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("Нельзя провести пустой документ")
# Создаем StockBatch напрямую для каждого товара
batches_created = []
total_cost = Decimal('0')
for item in document.items.select_related('product'):
# Создаем партию товара на складе
stock_batch = StockBatch.objects.create(
product=item.product,
warehouse=document.warehouse,
quantity=item.quantity,
cost_price=item.cost_price,
is_active=True
)
batches_created.append(stock_batch)
total_cost += item.total_cost
# Обновляем или создаем запись в Stock
stock, _ = Stock.objects.get_or_create(
product=item.product,
warehouse=document.warehouse
)
# Пересчитываем остаток из всех активных партий
stock.refresh_from_batches()
# Обновляем статус документа
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,
'batches_created': len(batches_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')