Добавлена система трансформации товаров
Реализована полная система трансформации товаров (превращение одного товара в другой). Пример: белая гипсофила → крашеная гипсофила. Особенности реализации: - Резервирование входных товаров в статусе draft - FIFO списание входных товаров при проведении - Автоматический расчёт себестоимости выходных товаров - Возможность отмены как черновиков, так и проведённых трансформаций Модели (inventory/models.py): - Transformation: документ трансформации (draft/completed/cancelled) - TransformationInput: входные товары (списание) - TransformationOutput: выходные товары (оприходование) - Добавлен статус 'converted_to_transformation' в Reservation - Добавлен тип 'transformation' в DocumentCounter Бизнес-логика (inventory/services/transformation_service.py): - TransformationService с методами CRUD - Валидация наличия товаров - Автоматическая генерация номеров документов Сигналы (inventory/signals.py): - Автоматическое резервирование входных товаров - FIFO списание при проведении - Создание партий выходных товаров - Откат операций при отмене Интерфейс без Django Admin: - Список трансформаций (list.html) - Форма создания (form.html) - Детальный просмотр с добавлением товаров (detail.html) - Интеграция с компонентом поиска товаров - 8 views для полного CRUD + проведение/отмена Миграция: - 0003_alter_documentcounter_counter_type_and_more.py 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
279
myproject/inventory/services/transformation_service.py
Normal file
279
myproject/inventory/services/transformation_service.py
Normal file
@@ -0,0 +1,279 @@
|
||||
"""
|
||||
Сервис для работы с трансформациями товаров (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
|
||||
Reference in New Issue
Block a user