Исправлена проблема, когда при отмене проведенной трансформации оба сигнала выполнялись последовательно:
- rollback_transformation_on_cancel возвращал резервы в 'reserved'
- release_reservations_on_draft_cancel ошибочно освобождал их в 'released'
Изменена проверка в release_reservations_on_draft_cancel: вместо проверки наличия партий Output (которые уже удалены) теперь проверяется статус резервов ('converted_to_transformation') или наличие поля converted_at, что работает независимо от порядка выполнения сигналов.
250 lines
10 KiB
Python
250 lines
10 KiB
Python
"""
|
||
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')
|