commit
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user