Добавлена система трансформации товаров
Реализована полная система трансформации товаров (превращение одного товара в другой). Пример: белая гипсофила → крашеная гипсофила. Особенности реализации: - Резервирование входных товаров в статусе 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:
237
myproject/inventory/views/transformation.py
Normal file
237
myproject/inventory/views/transformation.py
Normal file
@@ -0,0 +1,237 @@
|
||||
"""
|
||||
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')
|
||||
|
||||
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')
|
||||
Reference in New Issue
Block a user