This commit is contained in:
2025-11-04 11:00:05 +03:00
parent 706ee5d8e8
commit b24d5bcdee
13 changed files with 1383 additions and 72 deletions

View File

@@ -27,7 +27,7 @@ from .inventory_ops import (
InventoryLineCreateBulkView
)
from .writeoff import WriteOffListView, WriteOffCreateView, WriteOffUpdateView, WriteOffDeleteView
from .transfer import TransferListView, TransferCreateView, TransferUpdateView, TransferDeleteView
from .transfer import TransferListView, TransferBulkCreateView, TransferDetailView, TransferDeleteView, GetProductStockView
from .reservation import ReservationListView, ReservationCreateView, ReservationUpdateView
from .stock import StockListView, StockDetailView
from .allocation import SaleBatchAllocationListView
@@ -58,7 +58,7 @@ __all__ = [
# WriteOff
'WriteOffListView', 'WriteOffCreateView', 'WriteOffUpdateView', 'WriteOffDeleteView',
# Transfer
'TransferListView', 'TransferCreateView', 'TransferUpdateView', 'TransferDeleteView',
'TransferListView', 'TransferBulkCreateView', 'TransferDetailView', 'TransferDeleteView', 'GetProductStockView',
# Reservation
'ReservationListView', 'ReservationCreateView', 'ReservationUpdateView',
# Stock

View File

@@ -11,7 +11,7 @@ from django.utils.decorators import method_decorator
from django.db import IntegrityError, transaction
from ..models import Incoming, IncomingBatch, Warehouse
from ..forms import IncomingForm, IncomingLineForm
from ..utils import generate_incoming_document_number
from inventory.utils import generate_incoming_document_number
from products.models import Product
file_logger = logging.getLogger('incoming_sequence_file')

View File

@@ -3,58 +3,235 @@
Transfer (Перемещение товара между складами) views
GROUP 2: MEDIUM PRIORITY
"""
from django.views.generic import ListView, CreateView, UpdateView, DeleteView
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 ..models import Transfer
from ..forms import TransferForm
from django.http import JsonResponse
from django.db import transaction
from django.shortcuts import redirect, render
from ..models import TransferBatch, TransferItem, 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):
model = Transfer
"""
View для просмотра списка документов перемещений товаров.
"""
model = TransferBatch
template_name = 'inventory/transfer/transfer_list.html'
context_object_name = 'transfers'
paginate_by = 20
def get_queryset(self):
return Transfer.objects.select_related(
'batch', 'batch__product',
return TransferBatch.objects.select_related(
'from_warehouse', 'to_warehouse'
).order_by('-date')
).order_by('-created_at')
class TransferCreateView(LoginRequiredMixin, CreateView):
model = Transfer
form_class = TransferForm
template_name = 'inventory/transfer/transfer_form.html'
success_url = reverse_lazy('inventory:transfer-list')
# ============================================================================
# VIEWS ДЛЯ ПЕРЕМЕЩЕНИЯ ТОВАРОВ (TransferBatch + TransferItem)
# ============================================================================
def form_valid(self, form):
messages.success(
self.request,
f'Перемещение товара "{form.instance.batch.product.name}" успешно зарегистрировано.'
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(is_active=True).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, is_active=True)
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. Создаем документ TransferBatch
transfer_batch = TransferBatch.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
)
# Создаем TransferItem для каждого использованного batch
for source_batch, qty_transferred, new_batch in transfers:
TransferItem.objects.create(
transfer_batch=transfer_batch,
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_batch.document_number} успешно создан. '
f'Перемещено {len(products)} видов товаров.'
)
return redirect('inventory:transfer-detail', pk=transfer_batch.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 = TransferBatch
template_name = 'inventory/transfer/transfer_detail.html'
context_object_name = 'transfer_batch'
def get_queryset(self):
return TransferBatch.objects.select_related(
'from_warehouse', 'to_warehouse'
).prefetch_related(
'items__product', 'items__batch', 'items__new_batch'
)
return super().form_valid(form)
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
transfer_batch = self.object
class TransferUpdateView(LoginRequiredMixin, UpdateView):
model = Transfer
form_class = TransferForm
template_name = 'inventory/transfer/transfer_form.html'
success_url = reverse_lazy('inventory:transfer-list')
# Собираем статистику по документу
items = transfer_batch.items.all()
total_items = items.count()
total_qty = sum(Decimal(str(item.quantity)) for item in items)
def form_valid(self, form):
messages.success(self.request, f'Перемещение товара обновлено.')
return super().form_valid(form)
context['total_items'] = total_items
context['total_qty'] = total_qty
context['items'] = items
return context
class TransferDeleteView(LoginRequiredMixin, DeleteView):
model = Transfer
"""
View для удаления документа перемещения.
"""
model = TransferBatch
template_name = 'inventory/transfer/transfer_confirm_delete.html'
success_url = reverse_lazy('inventory:transfer-list')
def form_valid(self, form):
transfer = self.get_object()
messages.success(self.request, f'Перемещение товара отменено.')
transfer_batch = self.get_object()
messages.success(self.request, f'Документ перемещения {transfer_batch.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)