Добавлена система трансформации товаров

Реализована полная система трансформации товаров (превращение одного товара в другой).
Пример: белая гипсофила → крашеная гипсофила.

Особенности реализации:
- Резервирование входных товаров в статусе 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:
2025-12-25 18:27:31 +03:00
parent 56850e790e
commit 30ee077963
12 changed files with 1682 additions and 4 deletions

View 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