Унификация генерации номеров документов и оптимизация кода
- Унифицирован формат номеров документов: IN-XXXXXX (6 цифр), как WO-XXXXXX и MOVE-XXXXXX - Убрано дублирование функции _extract_number_from_document_number - Оптимизирована инициализация счетчика incoming: быстрая проверка перед полной инициализацией - Удален неиспользуемый файл utils.py (функциональность перенесена в document_generator.py) - Все функции генерации номеров используют единый подход через DocumentCounter.get_next_value()
This commit is contained in:
293
myproject/inventory/services/incoming_document_service.py
Normal file
293
myproject/inventory/services/incoming_document_service.py
Normal file
@@ -0,0 +1,293 @@
|
||||
"""
|
||||
Сервис для работы с документами поступления (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')
|
||||
|
||||
Reference in New Issue
Block a user