Files
octopus/myproject/inventory/services/transformation_service.py
Andrey Smakotin bc13750d16 Исправление конфликта сигналов при отмене трансформации
Исправлена проблема, когда при отмене проведенной трансформации оба сигнала выполнялись последовательно:
- rollback_transformation_on_cancel возвращал резервы в 'reserved'
- release_reservations_on_draft_cancel ошибочно освобождал их в 'released'

Изменена проверка в release_reservations_on_draft_cancel: вместо проверки наличия партий Output (которые уже удалены) теперь проверяется статус резервов ('converted_to_transformation') или наличие поля converted_at, что работает независимо от порядка выполнения сигналов.
2025-12-25 22:54:39 +03:00

294 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("Добавьте хотя бы один выходной товар")
# Проверяем что сумма входных количеств равна сумме выходных
total_input_quantity = sum(
trans_input.quantity for trans_input in transformation.inputs.all()
)
total_output_quantity = sum(
trans_output.quantity for trans_output in transformation.outputs.all()
)
if total_input_quantity != total_output_quantity:
raise ValidationError(
f"Сумма входных количеств ({total_input_quantity}) должна быть равна "
f"сумме выходных количеств ({total_output_quantity})"
)
# Проверяем наличие всех входных товаров
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