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

238 lines
9.9 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.
"""
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')