Унификация генерации номеров документов и оптимизация кода

- Унифицирован формат номеров документов: 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:
2025-12-21 00:51:08 +03:00
parent 78dc9e9801
commit 375ec5366a
14 changed files with 1873 additions and 147 deletions

View 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')