Files
octopus/myproject/inventory/views/transfer.py
Andrey Smakotin 08bae834c8 refactor: стандартизация моделей документов перемещения
Приведение к единому паттерну именования документов:
- TransferBatch → TransferDocument
- TransferItem → TransferDocumentItem
- Удалена устаревшая модель Transfer (одиночные перемещения)
- Удалена неиспользуемая модель StockMovement

Изменения:
- models.py: переименование классов, обновление related_names
- admin.py: удаление регистраций Transfer/StockMovement
- forms.py: обновление TransferHeaderForm
- views/transfer.py: обновление всех view классов
- templates: замена transfer_batch → transfer_document
- urls.py: удаление путей для movements
- views/__init__.py: удаление импорта StockMovementListView
- views/movements.py: удален файл

Миграция: 0005_refactor_transfer_models
- RenameModel операции для сохранения данных
- DeleteModel для Transfer и StockMovement

Единый паттерн: *Document + *DocumentItem
(WriteOffDocument, IncomingDocument, TransferDocument)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-26 20:29:11 +03:00

238 lines
10 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.
# -*- coding: utf-8 -*-
"""
Transfer (Перемещение товара между складами) views
GROUP 2: MEDIUM PRIORITY
"""
import json
from decimal import Decimal
from django.views.generic import ListView, CreateView, UpdateView, DeleteView, DetailView
from django.views import View
from django.urls import reverse_lazy
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib import messages
from django.http import JsonResponse
from django.db import transaction
from django.shortcuts import redirect, render
from ..models import TransferDocument, TransferDocumentItem, Stock
from ..forms import TransferBulkForm
from inventory.utils.document_generator import generate_transfer_document_number
from inventory.services.batch_manager import StockBatchManager
from products.models import Product
class TransferListView(LoginRequiredMixin, ListView):
"""
View для просмотра списка документов перемещений товаров.
"""
model = TransferDocument
template_name = 'inventory/transfer/transfer_list.html'
context_object_name = 'transfers'
paginate_by = 20
def get_queryset(self):
return TransferDocument.objects.select_related(
'from_warehouse', 'to_warehouse'
).order_by('-created_at')
# ============================================================================
# VIEWS ДЛЯ ПЕРЕМЕЩЕНИЯ ТОВАРОВ (TransferDocument + TransferDocumentItem)
# ============================================================================
class TransferBulkCreateView(LoginRequiredMixin, View):
"""
View для создания документа перемещения товаров между складами с FIFO логикой.
Один документ может содержать несколько товаров.
"""
template_name = 'inventory/transfer/transfer_bulk_form.html'
def get(self, request):
from products.models import Product
form = TransferBulkForm()
products = Product.objects.filter(status='active').values('id', 'name', 'sku').order_by('name')
return render(request, self.template_name, {
'form': form,
'products': products
})
def post(self, request):
form = TransferBulkForm(request.POST)
if not form.is_valid():
messages.error(request, 'Ошибка при заполнении формы')
return render(request, self.template_name, {'form': form}, status=400)
# Получаем данные из формы
from_warehouse = form.cleaned_data.get('from_warehouse')
to_warehouse = form.cleaned_data.get('to_warehouse')
notes = form.cleaned_data.get('notes', '')
# Парсим JSON с товарами
products_json = request.POST.get('products_json', '[]')
try:
products_data = json.loads(products_json)
except json.JSONDecodeError:
messages.error(request, 'Ошибка при парсинге товаров. Пожалуйста, проверьте данные.')
return render(request, self.template_name, {'form': form}, status=400)
# Проверяем что товары есть
if not products_data:
messages.error(request, 'Необходимо добавить хотя бы один товар для перемещения.')
return render(request, self.template_name, {'form': form}, status=400)
# Валидируем товары
products = []
for item in products_data:
product_id = item.get('product_id')
quantity = Decimal(str(item.get('quantity', 0)))
if quantity <= 0:
messages.error(request, f'Количество товара должно быть больше нуля')
return render(request, self.template_name, {'form': form}, status=400)
try:
product = Product.objects.get(id=product_id, status='active')
products.append((product, quantity))
except Product.DoesNotExist:
messages.error(request, f'Товар с ID {product_id} не найден или неактивен')
return render(request, self.template_name, {'form': form}, status=400)
# Начинаем транзакцию
try:
with transaction.atomic():
# 1. Создаем документ TransferDocument
transfer_document = TransferDocument.objects.create(
from_warehouse=from_warehouse,
to_warehouse=to_warehouse,
document_number=generate_transfer_document_number(),
notes=notes
)
# 2. Для каждого товара выполняем FIFO перемещение
for product, quantity in products:
try:
# Получаем список распределений по FIFO
transfers = StockBatchManager.transfer_product_by_fifo(
product=product,
from_warehouse=from_warehouse,
to_warehouse=to_warehouse,
quantity=quantity
)
# Создаем TransferDocumentItem для каждого использованного batch
for source_batch, qty_transferred, new_batch in transfers:
TransferDocumentItem.objects.create(
transfer_document=transfer_document,
product=product,
batch=source_batch,
quantity=qty_transferred,
new_batch=new_batch
)
except ValueError as e:
messages.error(request, f'Ошибка при перемещении товара "{product.name}": {str(e)}')
raise # Откатываем транзакцию
# 3. Успешно создали документ
messages.success(
request,
f'Документ перемещения {transfer_document.document_number} успешно создан. '
f'Перемещено {len(products)} видов товаров.'
)
return redirect('inventory:transfer-detail', pk=transfer_document.id)
except Exception as e:
messages.error(request, f'Ошибка при создании документа перемещения: {str(e)}')
return render(request, self.template_name, {'form': form}, status=400)
class TransferDetailView(LoginRequiredMixin, DetailView):
"""
View для просмотра деталей документа перемещения.
"""
model = TransferDocument
template_name = 'inventory/transfer/transfer_detail.html'
context_object_name = 'transfer_document'
def get_queryset(self):
return TransferDocument.objects.select_related(
'from_warehouse', 'to_warehouse'
).prefetch_related(
'items__product', 'items__batch', 'items__new_batch'
)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
transfer_document = self.object
# Собираем статистику по документу
items = transfer_document.items.all()
total_items = items.count()
total_qty = sum(Decimal(str(item.quantity)) for item in items)
context['total_items'] = total_items
context['total_qty'] = total_qty
context['items'] = items
return context
class TransferDeleteView(LoginRequiredMixin, DeleteView):
"""
View для удаления документа перемещения.
"""
model = TransferDocument
template_name = 'inventory/transfer/transfer_confirm_delete.html'
success_url = reverse_lazy('inventory:transfer-list')
def form_valid(self, form):
transfer_document = self.get_object()
messages.success(self.request, f'Документ перемещения {transfer_document.document_number} удалён.')
return super().form_valid(form)
class GetProductStockView(LoginRequiredMixin, View):
"""
API endpoint для получения доступного количества товара на конкретном складе.
GET параметры: product_id, warehouse_id
Возвращает JSON: {"quantity": "100.000", "warehouse_name": "Основной склад"}
"""
def get(self, request):
product_id = request.GET.get('product_id')
warehouse_id = request.GET.get('warehouse_id')
if not product_id or not warehouse_id:
return JsonResponse({
'error': 'Missing required parameters: product_id, warehouse_id'
}, status=400)
try:
product_id = int(product_id)
warehouse_id = int(warehouse_id)
except ValueError:
return JsonResponse({
'error': 'Invalid parameter values'
}, status=400)
try:
stock = Stock.objects.get(product_id=product_id, warehouse_id=warehouse_id)
return JsonResponse({
'quantity': str(stock.quantity_available),
'warehouse_name': stock.warehouse.name,
'success': True
})
except Stock.DoesNotExist:
return JsonResponse({
'quantity': '0.000',
'warehouse_name': '',
'success': True
})
except Exception as e:
import traceback
traceback.print_exc()
return JsonResponse({
'error': str(e),
'success': False
}, status=500)