Files
octopus/myproject/inventory/views/transformation.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

250 lines
10 KiB
Python
Raw Permalink 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.
"""
Views для работы с трансформациями товаров (Transformation).
"""
from django.views.generic import ListView, CreateView, DetailView, View
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse
from django.shortcuts import get_object_or_404, redirect
from django.contrib import messages
from django.http import JsonResponse
from django.db import transaction
from django.core.exceptions import ValidationError
from inventory.models import Transformation, TransformationInput, TransformationOutput
from inventory.forms import TransformationForm, TransformationInputForm, TransformationOutputForm
from inventory.services.transformation_service import TransformationService
class TransformationListView(LoginRequiredMixin, ListView):
"""Список трансформаций товаров"""
model = Transformation
template_name = 'inventory/transformation/list.html'
context_object_name = 'transformations'
paginate_by = 20
def get_queryset(self):
return Transformation.objects.select_related(
'warehouse', 'employee'
).prefetch_related('inputs__product', 'outputs__product').order_by('-date')
class TransformationCreateView(LoginRequiredMixin, CreateView):
"""Создание трансформации"""
model = Transformation
form_class = TransformationForm
template_name = 'inventory/transformation/form.html'
def form_valid(self, form):
transformation = TransformationService.create_transformation(
warehouse=form.cleaned_data['warehouse'],
comment=form.cleaned_data.get('comment'),
employee=self.request.user
)
messages.success(self.request, f'Трансформация {transformation.document_number} создана')
return redirect('inventory:transformation-detail', pk=transformation.pk)
class TransformationDetailView(LoginRequiredMixin, DetailView):
"""Детальный просмотр трансформации"""
model = Transformation
template_name = 'inventory/transformation/detail.html'
context_object_name = 'transformation'
def get_queryset(self):
return Transformation.objects.select_related(
'warehouse', 'employee'
).prefetch_related('inputs__product', 'outputs__product')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['input_form'] = TransformationInputForm(transformation=self.object)
context['output_form'] = TransformationOutputForm(transformation=self.object)
# Добавляем категории и теги для компонента поиска товаров
from products.models import ProductCategory, ProductTag
context['categories'] = ProductCategory.objects.filter(is_active=True).order_by('name')
context['tags'] = ProductTag.objects.filter(is_active=True).order_by('name')
# Вычисляем суммы для подсказки
from decimal import Decimal
total_input = sum(
trans_input.quantity for trans_input in self.object.inputs.all()
)
total_output = sum(
trans_output.quantity for trans_output in self.object.outputs.all()
)
context['total_input_quantity'] = total_input
context['total_output_quantity'] = total_output
context['quantity_balance'] = total_input - total_output
return context
class TransformationAddInputView(LoginRequiredMixin, View):
"""Добавление входного товара в трансформацию"""
@transaction.atomic
def post(self, request, pk):
transformation = get_object_or_404(Transformation, pk=pk)
form = TransformationInputForm(request.POST, transformation=transformation)
if form.is_valid():
try:
trans_input = TransformationService.add_input(
transformation=transformation,
product=form.cleaned_data['product'],
quantity=form.cleaned_data['quantity']
)
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({
'success': True,
'item_id': trans_input.id,
'message': f'Добавлено: {trans_input.product.name}'
})
messages.success(request, f'Добавлено: {trans_input.product.name}')
except ValidationError as e:
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({'success': False, 'error': str(e)}, status=400)
messages.error(request, str(e))
else:
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
errors = '; '.join([f"{k}: {v[0]}" for k, v in form.errors.items()])
return JsonResponse({'success': False, 'error': errors}, status=400)
for field, errors in form.errors.items():
for error in errors:
messages.error(request, f'{field}: {error}')
return redirect('inventory:transformation-detail', pk=pk)
class TransformationAddOutputView(LoginRequiredMixin, View):
"""Добавление выходного товара в трансформацию"""
@transaction.atomic
def post(self, request, pk):
transformation = get_object_or_404(Transformation, pk=pk)
form = TransformationOutputForm(request.POST, transformation=transformation)
if form.is_valid():
try:
trans_output = TransformationService.add_output(
transformation=transformation,
product=form.cleaned_data['product'],
quantity=form.cleaned_data['quantity']
)
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({
'success': True,
'item_id': trans_output.id,
'message': f'Добавлено: {trans_output.product.name}'
})
messages.success(request, f'Добавлено: {trans_output.product.name}')
except ValidationError as e:
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({'success': False, 'error': str(e)}, status=400)
messages.error(request, str(e))
else:
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
errors = '; '.join([f"{k}: {v[0]}" for k, v in form.errors.items()])
return JsonResponse({'success': False, 'error': errors}, status=400)
for field, errors in form.errors.items():
for error in errors:
messages.error(request, f'{field}: {error}')
return redirect('inventory:transformation-detail', pk=pk)
class TransformationRemoveInputView(LoginRequiredMixin, View):
"""Удаление входного товара из трансформации"""
@transaction.atomic
def post(self, request, pk, item_pk):
transformation = get_object_or_404(Transformation, pk=pk)
trans_input = get_object_or_404(TransformationInput, pk=item_pk, transformation=transformation)
try:
product_name = trans_input.product.name
TransformationService.remove_input(trans_input)
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({
'success': True,
'message': f'Удалено: {product_name}'
})
messages.success(request, f'Удалено: {product_name}')
except ValidationError as e:
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({'success': False, 'error': str(e)}, status=400)
messages.error(request, str(e))
return redirect('inventory:transformation-detail', pk=pk)
class TransformationRemoveOutputView(LoginRequiredMixin, View):
"""Удаление выходного товара из трансформации"""
@transaction.atomic
def post(self, request, pk, item_pk):
transformation = get_object_or_404(Transformation, pk=pk)
trans_output = get_object_or_404(TransformationOutput, pk=item_pk, transformation=transformation)
try:
product_name = trans_output.product.name
TransformationService.remove_output(trans_output)
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({
'success': True,
'message': f'Удалено: {product_name}'
})
messages.success(request, f'Удалено: {product_name}')
except ValidationError as e:
if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
return JsonResponse({'success': False, 'error': str(e)}, status=400)
messages.error(request, str(e))
return redirect('inventory:transformation-detail', pk=pk)
class TransformationConfirmView(LoginRequiredMixin, View):
"""Проведение трансформации"""
@transaction.atomic
def post(self, request, pk):
transformation = get_object_or_404(Transformation, pk=pk)
try:
TransformationService.confirm(transformation)
messages.success(request, f'Трансформация {transformation.document_number} проведена')
except ValidationError as e:
messages.error(request, str(e))
return redirect('inventory:transformation-detail', pk=pk)
class TransformationCancelView(LoginRequiredMixin, View):
"""Отмена трансформации"""
@transaction.atomic
def post(self, request, pk):
transformation = get_object_or_404(Transformation, pk=pk)
try:
TransformationService.cancel(transformation)
messages.success(request, f'Трансформация {transformation.document_number} отменена')
except ValidationError as e:
messages.error(request, str(e))
return redirect('inventory:transformation-list')