Files
octopus/myproject/inventory/services/transformation_service.py
Andrey Smakotin 30ee077963 Добавлена система трансформации товаров
Реализована полная система трансформации товаров (превращение одного товара в другой).
Пример: белая гипсофила → крашеная гипсофила.

Особенности реализации:
- Резервирование входных товаров в статусе 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>
2025-12-25 18:27:31 +03:00

280 lines
11 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.
"""
Сервис для работы с трансформациями товаров (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