- Добавлены поля snapshot_* в модель InventoryLine для фиксации значений на момент завершения - Обновлен InventoryProcessor для сохранения snapshot перед обработкой - Обновлен InventoryDetailView для отображения snapshot-значений в проведенных инвентаризациях - Добавлена миграция 0018 для новых полей - Теперь в проведенных инвентаризациях отображаются оригинальные значения и правильная разница, а не текущие скорректированные остатки
665 lines
31 KiB
Python
665 lines
31 KiB
Python
# -*- coding: utf-8 -*-
|
||
from django.shortcuts import render, redirect, get_object_or_404
|
||
from django.views.generic import ListView, CreateView, DetailView, View, FormView
|
||
from django.urls import reverse_lazy
|
||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||
from django.contrib import messages
|
||
from django.http import HttpResponseRedirect, JsonResponse
|
||
from django.db import transaction
|
||
from django.views.decorators.http import require_http_methods
|
||
from django.utils.decorators import method_decorator
|
||
from decimal import Decimal
|
||
import json
|
||
|
||
from ..models import Inventory, InventoryLine, Stock
|
||
from ..forms import InventoryForm, InventoryLineForm
|
||
from ..services.inventory_processor import InventoryProcessor
|
||
from ..services.writeoff_document_service import WriteOffDocumentService
|
||
from ..services.incoming_document_service import IncomingDocumentService
|
||
from django.core.exceptions import ValidationError
|
||
from products.models import Product, ProductCategory, ProductTag
|
||
|
||
|
||
class InventoryListView(LoginRequiredMixin, ListView):
|
||
"""
|
||
Список всех инвентаризаций по складам
|
||
"""
|
||
model = Inventory
|
||
template_name = 'inventory/inventory/inventory_list.html'
|
||
context_object_name = 'inventories'
|
||
paginate_by = 20
|
||
|
||
def get_queryset(self):
|
||
queryset = Inventory.objects.select_related('warehouse').order_by('-date')
|
||
|
||
# Фильтры (если переданы)
|
||
warehouse_id = self.request.GET.get('warehouse')
|
||
status = self.request.GET.get('status')
|
||
|
||
if warehouse_id:
|
||
queryset = queryset.filter(warehouse_id=warehouse_id)
|
||
if status:
|
||
queryset = queryset.filter(status=status)
|
||
|
||
return queryset
|
||
|
||
|
||
class InventoryCreateView(LoginRequiredMixin, CreateView):
|
||
"""
|
||
Начало новой инвентаризации по конкретному складу.
|
||
Переводит инвентаризацию в статус 'processing'.
|
||
"""
|
||
model = Inventory
|
||
form_class = InventoryForm
|
||
template_name = 'inventory/inventory/inventory_form.html'
|
||
|
||
def form_valid(self, form):
|
||
from inventory.utils.document_generator import generate_inventory_document_number
|
||
|
||
form.instance.status = 'processing'
|
||
form.instance.document_number = generate_inventory_document_number()
|
||
|
||
# Автоматически проставляем роль пользователя, который создает инвентаризацию
|
||
# Если у пользователя есть роль в тенанте - проставляем её, иначе оставляем NULL
|
||
try:
|
||
if hasattr(self.request.user, 'tenant_role'):
|
||
user_role = getattr(self.request.user, 'tenant_role', None)
|
||
if user_role and user_role.is_active:
|
||
form.instance.conducted_by = user_role
|
||
except (AttributeError, Exception):
|
||
# Если у пользователя нет роли (суперюзер/админ без роли) - оставляем NULL
|
||
pass
|
||
|
||
messages.success(
|
||
self.request,
|
||
f'Инвентаризация склада "{form.instance.warehouse.name}" начата.'
|
||
)
|
||
return super().form_valid(form)
|
||
|
||
def get_success_url(self):
|
||
"""Перенаправляем на страницу редактирования созданной инвентаризации"""
|
||
return reverse_lazy('inventory:inventory-detail', kwargs={'pk': self.object.pk})
|
||
|
||
|
||
class InventoryDetailView(LoginRequiredMixin, DetailView):
|
||
"""
|
||
Детальный просмотр инвентаризации с её строками.
|
||
Позволяет добавлять строки и заполнять фактические количества.
|
||
"""
|
||
model = Inventory
|
||
template_name = 'inventory/inventory/inventory_detail.html'
|
||
context_object_name = 'inventory'
|
||
|
||
def get_queryset(self):
|
||
"""Оптимизация: предзагружаем warehouse"""
|
||
return Inventory.objects.select_related('warehouse')
|
||
|
||
def get_context_data(self, **kwargs):
|
||
context = super().get_context_data(**kwargs)
|
||
from inventory.models import Stock, StockBatch, Reservation
|
||
from django.db.models import Sum, Q
|
||
from decimal import Decimal
|
||
|
||
# Получаем все строки этой инвентаризации с оптимизацией
|
||
lines = InventoryLine.objects.filter(
|
||
inventory=self.object
|
||
).select_related('product').order_by('product__name')
|
||
|
||
if not lines.exists():
|
||
context['lines'] = []
|
||
context['categories'] = ProductCategory.objects.filter(is_active=True).order_by('name')
|
||
context['tags'] = ProductTag.objects.filter(is_active=True).order_by('name')
|
||
if self.object.status == 'completed':
|
||
context['writeoff_document'] = self.object.writeoff_documents.filter(status='draft').first()
|
||
context['incoming_document'] = self.object.incoming_documents.filter(status='draft').first()
|
||
return context
|
||
|
||
# Получаем все product_id из строк
|
||
product_ids = [line.product_id for line in lines]
|
||
warehouse = self.object.warehouse
|
||
|
||
# Получаем или создаем все Stock объекты одним запросом
|
||
stocks = Stock.objects.filter(
|
||
product_id__in=product_ids,
|
||
warehouse=warehouse
|
||
)
|
||
existing_stocks = {stock.product_id: stock for stock in stocks}
|
||
|
||
# Создаем недостающие Stock объекты
|
||
missing_product_ids = set(product_ids) - set(existing_stocks.keys())
|
||
if missing_product_ids:
|
||
new_stocks = [
|
||
Stock(product_id=pid, warehouse=warehouse)
|
||
for pid in missing_product_ids
|
||
]
|
||
Stock.objects.bulk_create(new_stocks)
|
||
# Перезагружаем все Stock объекты
|
||
stocks = Stock.objects.filter(
|
||
product_id__in=product_ids,
|
||
warehouse=warehouse
|
||
)
|
||
existing_stocks = {stock.product_id: stock for stock in stocks}
|
||
|
||
# Bulk запрос для получения всех StockBatch данных
|
||
stock_batches_qs = StockBatch.objects.filter(
|
||
product_id__in=product_ids,
|
||
warehouse=warehouse,
|
||
is_active=True
|
||
).values('product_id').annotate(
|
||
total_qty=Sum('quantity')
|
||
)
|
||
stock_batches_dict = {
|
||
item['product_id']: item['total_qty'] or Decimal('0')
|
||
for item in stock_batches_qs
|
||
}
|
||
|
||
# Bulk запрос для получения всех Reservation данных
|
||
reservations_qs = Reservation.objects.filter(
|
||
product_id__in=product_ids,
|
||
warehouse=warehouse,
|
||
status='reserved'
|
||
).values('product_id').annotate(
|
||
total_reserved=Sum('quantity')
|
||
)
|
||
reservations_dict = {
|
||
item['product_id']: item['total_reserved'] or Decimal('0')
|
||
for item in reservations_qs
|
||
}
|
||
|
||
# Обновляем все Stock объекты bulk update
|
||
stocks_to_update = []
|
||
for stock in stocks:
|
||
product_id = stock.product_id
|
||
stock.quantity_available = stock_batches_dict.get(product_id, Decimal('0'))
|
||
stock.quantity_reserved = reservations_dict.get(product_id, Decimal('0'))
|
||
stocks_to_update.append(stock)
|
||
|
||
if stocks_to_update:
|
||
Stock.objects.bulk_update(
|
||
stocks_to_update,
|
||
['quantity_available', 'quantity_reserved', 'updated_at'],
|
||
batch_size=100
|
||
)
|
||
|
||
# Добавляем quantity_reserved и обновляем quantity_system для каждой строки
|
||
lines_with_reserved = []
|
||
for line in lines:
|
||
# Для завершенных инвентаризаций используем snapshot-значения
|
||
if self.object.status == 'completed':
|
||
# Используем snapshot-значения если они есть (для новых записей)
|
||
if line.snapshot_quantity_available is not None:
|
||
line.quantity_available = line.snapshot_quantity_available
|
||
line.quantity_reserved = line.snapshot_quantity_reserved
|
||
line.quantity_system = line.snapshot_quantity_system
|
||
# Используем snapshot_difference, если он есть, иначе пересчитываем
|
||
if line.snapshot_difference is not None:
|
||
line.difference = line.snapshot_difference
|
||
else:
|
||
# Fallback: пересчитываем из snapshot-значений
|
||
line.difference = (line.quantity_fact + line.snapshot_quantity_reserved) - line.snapshot_quantity_available
|
||
else:
|
||
# Fallback для старых записей без snapshot
|
||
# Используем текущие значения Stock, но это не идеально
|
||
# так как остатки уже могли измениться после завершения инвентаризации
|
||
stock = existing_stocks.get(line.product_id)
|
||
if not stock:
|
||
stock, _ = Stock.objects.get_or_create(
|
||
product=line.product,
|
||
warehouse=warehouse
|
||
)
|
||
stock.refresh_from_batches()
|
||
line.quantity_reserved = stock.quantity_reserved
|
||
line.quantity_available = stock.quantity_available
|
||
# Для старых записей используем сохраненное quantity_system из модели
|
||
# и пересчитываем разницу
|
||
line.difference = (line.quantity_fact + line.quantity_reserved) - line.quantity_available
|
||
else:
|
||
# Для незавершенных инвентаризаций используем актуальные значения
|
||
stock = existing_stocks.get(line.product_id)
|
||
if not stock:
|
||
stock, _ = Stock.objects.get_or_create(
|
||
product=line.product,
|
||
warehouse=warehouse
|
||
)
|
||
stock.refresh_from_batches()
|
||
|
||
# Используем актуальное свободное количество из Stock
|
||
line.quantity_system = stock.quantity_free
|
||
line.quantity_reserved = stock.quantity_reserved
|
||
line.quantity_available = stock.quantity_available
|
||
|
||
# Пересчитываем разницу по новой формуле: (quantity_fact + quantity_reserved) - quantity_available
|
||
line.difference = (line.quantity_fact + line.quantity_reserved) - line.quantity_available
|
||
|
||
lines_with_reserved.append(line)
|
||
|
||
context['lines'] = lines_with_reserved
|
||
|
||
# Получаем категории и теги для компонента поиска товаров
|
||
context['categories'] = ProductCategory.objects.filter(is_active=True).order_by('name')
|
||
context['tags'] = ProductTag.objects.filter(is_active=True).order_by('name')
|
||
|
||
# Получаем созданные документы (если инвентаризация завершена)
|
||
if self.object.status == 'completed':
|
||
context['writeoff_document'] = self.object.writeoff_documents.first()
|
||
context['incoming_document'] = self.object.incoming_documents.first()
|
||
|
||
return context
|
||
|
||
|
||
class InventoryLineCreateBulkView(LoginRequiredMixin, View):
|
||
"""
|
||
Форма для массового внесения результатов инвентаризации.
|
||
Позволяет заполнить результаты пересчета для всех товаров на складе.
|
||
"""
|
||
template_name = 'inventory/inventory/inventory_line_bulk_form.html'
|
||
|
||
def get_context_data(self, inventory_id, **kwargs):
|
||
inventory = Inventory.objects.get(pk=inventory_id)
|
||
return {
|
||
'inventory': inventory,
|
||
'products': inventory.warehouse.stock_batches.values_list(
|
||
'product', flat=True
|
||
).distinct()
|
||
}
|
||
|
||
def get(self, request, pk):
|
||
inventory = Inventory.objects.get(pk=pk)
|
||
context = {
|
||
'inventory': inventory,
|
||
'lines': InventoryLine.objects.filter(inventory=inventory).select_related('product')
|
||
}
|
||
return render(request, self.template_name, context)
|
||
|
||
def post(self, request, pk):
|
||
inventory = Inventory.objects.get(pk=pk)
|
||
# Здесь будет логика обработки массового ввода данных
|
||
# TODO: Реализовать обработку формы с множественными строками
|
||
messages.success(request, 'Результаты инвентаризации добавлены.')
|
||
return redirect('inventory:inventory-detail', pk=pk)
|
||
|
||
|
||
class InventoryLineAddView(LoginRequiredMixin, View):
|
||
"""
|
||
AJAX view для добавления строки инвентаризации.
|
||
Принимает product_id через POST, создает InventoryLine с quantity_system = quantity_free.
|
||
"""
|
||
|
||
@method_decorator(require_http_methods(["POST"]))
|
||
@transaction.atomic
|
||
def dispatch(self, *args, **kwargs):
|
||
return super().dispatch(*args, **kwargs)
|
||
|
||
def post(self, request, inventory_id):
|
||
try:
|
||
inventory = get_object_or_404(Inventory, id=inventory_id)
|
||
|
||
# Проверяем что инвентаризация не завершена
|
||
if inventory.status == 'completed':
|
||
return JsonResponse({
|
||
'success': False,
|
||
'error': 'Нельзя добавлять строки в завершенную инвентаризацию'
|
||
}, status=400)
|
||
|
||
# Получаем product_id из POST
|
||
product_id = request.POST.get('product_id')
|
||
if not product_id:
|
||
return JsonResponse({
|
||
'success': False,
|
||
'error': 'Не указан product_id'
|
||
}, status=400)
|
||
|
||
product = get_object_or_404(Product, id=product_id)
|
||
|
||
# Проверяем, нет ли уже такой строки
|
||
existing_line = InventoryLine.objects.filter(
|
||
inventory=inventory,
|
||
product=product
|
||
).first()
|
||
|
||
if existing_line:
|
||
return JsonResponse({
|
||
'success': False,
|
||
'error': f'Товар "{product.name}" уже добавлен в инвентаризацию'
|
||
}, status=400)
|
||
|
||
# Получаем свободное количество (quantity_free)
|
||
stock, _ = Stock.objects.get_or_create(
|
||
product=product,
|
||
warehouse=inventory.warehouse
|
||
)
|
||
stock.refresh_from_batches() # Обновить из партий
|
||
quantity_system = stock.quantity_free # Свободное незарезервированное количество
|
||
quantity_reserved = stock.quantity_reserved
|
||
quantity_available = stock.quantity_available
|
||
|
||
# Создаем строку инвентаризации
|
||
# Передаем quantity_reserved и quantity_available для корректного расчета разницы в save()
|
||
line = InventoryLine(
|
||
inventory=inventory,
|
||
product=product,
|
||
quantity_system=quantity_system,
|
||
quantity_fact=Decimal('0'), # Оператор заполнит позже
|
||
processed=False
|
||
)
|
||
line.save(quantity_reserved=quantity_reserved, quantity_available=quantity_available)
|
||
|
||
return JsonResponse({
|
||
'success': True,
|
||
'line': {
|
||
'id': line.id,
|
||
'product_id': product.id,
|
||
'product_name': product.name,
|
||
'quantity_system': str(line.quantity_system),
|
||
'quantity_reserved': str(quantity_reserved),
|
||
'quantity_available': str(quantity_available),
|
||
'quantity_fact': str(line.quantity_fact),
|
||
'difference': str(line.difference),
|
||
'processed': line.processed
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
return JsonResponse({
|
||
'success': False,
|
||
'error': str(e)
|
||
}, status=500)
|
||
|
||
|
||
class InventoryLineUpdateView(LoginRequiredMixin, View):
|
||
"""
|
||
AJAX view для обновления quantity_fact строки инвентаризации.
|
||
"""
|
||
|
||
@method_decorator(require_http_methods(["POST"]))
|
||
@transaction.atomic
|
||
def dispatch(self, *args, **kwargs):
|
||
return super().dispatch(*args, **kwargs)
|
||
|
||
def post(self, request, inventory_id, line_id):
|
||
try:
|
||
inventory = get_object_or_404(Inventory, id=inventory_id)
|
||
|
||
# Проверяем что инвентаризация не завершена
|
||
if inventory.status == 'completed':
|
||
return JsonResponse({
|
||
'success': False,
|
||
'error': 'Нельзя редактировать строки завершенной инвентаризации'
|
||
}, status=400)
|
||
|
||
line = get_object_or_404(InventoryLine, id=line_id, inventory=inventory)
|
||
|
||
# Проверяем что строка не обработана
|
||
if line.processed:
|
||
return JsonResponse({
|
||
'success': False,
|
||
'error': 'Нельзя редактировать обработанную строку'
|
||
}, status=400)
|
||
|
||
# Получаем quantity_fact из POST
|
||
quantity_fact_str = request.POST.get('quantity_fact')
|
||
if quantity_fact_str is None:
|
||
return JsonResponse({
|
||
'success': False,
|
||
'error': 'Не указано quantity_fact'
|
||
}, status=400)
|
||
|
||
try:
|
||
quantity_fact = Decimal(str(quantity_fact_str))
|
||
if quantity_fact < 0:
|
||
return JsonResponse({
|
||
'success': False,
|
||
'error': 'Количество не может быть отрицательным'
|
||
}, status=400)
|
||
except (ValueError, TypeError):
|
||
return JsonResponse({
|
||
'success': False,
|
||
'error': 'Некорректное значение quantity_fact'
|
||
}, status=400)
|
||
|
||
# Обновляем quantity_fact
|
||
line.quantity_fact = quantity_fact
|
||
|
||
# Получаем актуальные данные из Stock для расчета разницы
|
||
stock, _ = Stock.objects.get_or_create(
|
||
product=line.product,
|
||
warehouse=inventory.warehouse
|
||
)
|
||
stock.refresh_from_batches()
|
||
|
||
# Для незавершенных инвентаризаций обновляем quantity_system динамически
|
||
if inventory.status != 'completed':
|
||
line.quantity_system = stock.quantity_free
|
||
|
||
# Пересчитываем разницу по новой формуле: (quantity_fact + quantity_reserved) - quantity_available
|
||
line.difference = (line.quantity_fact + stock.quantity_reserved) - stock.quantity_available
|
||
|
||
# Сохраняем с передачей quantity_reserved и quantity_available для корректного расчета в save()
|
||
line.save(quantity_reserved=stock.quantity_reserved, quantity_available=stock.quantity_available)
|
||
|
||
return JsonResponse({
|
||
'success': True,
|
||
'line': {
|
||
'id': line.id,
|
||
'product_id': line.product.id,
|
||
'product_name': line.product.name,
|
||
'quantity_system': str(line.quantity_system),
|
||
'quantity_reserved': str(stock.quantity_reserved),
|
||
'quantity_available': str(stock.quantity_available),
|
||
'quantity_fact': str(line.quantity_fact),
|
||
'difference': str(line.difference),
|
||
'processed': line.processed
|
||
}
|
||
})
|
||
|
||
except Exception as e:
|
||
return JsonResponse({
|
||
'success': False,
|
||
'error': str(e)
|
||
}, status=500)
|
||
|
||
|
||
class InventoryLineDeleteView(LoginRequiredMixin, View):
|
||
"""
|
||
AJAX view для удаления строки инвентаризации.
|
||
"""
|
||
|
||
@method_decorator(require_http_methods(["POST"]))
|
||
@transaction.atomic
|
||
def dispatch(self, *args, **kwargs):
|
||
return super().dispatch(*args, **kwargs)
|
||
|
||
def post(self, request, inventory_id, line_id):
|
||
try:
|
||
inventory = get_object_or_404(Inventory, id=inventory_id)
|
||
|
||
# Проверяем что инвентаризация не завершена
|
||
if inventory.status == 'completed':
|
||
return JsonResponse({
|
||
'success': False,
|
||
'error': 'Нельзя удалять строки завершенной инвентаризации'
|
||
}, status=400)
|
||
|
||
line = get_object_or_404(InventoryLine, id=line_id, inventory=inventory)
|
||
|
||
# Проверяем что строка не обработана
|
||
if line.processed:
|
||
return JsonResponse({
|
||
'success': False,
|
||
'error': 'Нельзя удалять обработанную строку'
|
||
}, status=400)
|
||
|
||
product_name = line.product.name
|
||
line.delete()
|
||
|
||
return JsonResponse({
|
||
'success': True,
|
||
'message': f'Строка для товара "{product_name}" удалена'
|
||
})
|
||
|
||
except Exception as e:
|
||
return JsonResponse({
|
||
'success': False,
|
||
'error': str(e)
|
||
}, status=500)
|
||
|
||
|
||
class InventoryCompleteView(LoginRequiredMixin, View):
|
||
"""
|
||
View для завершения инвентаризации.
|
||
Создает документы списания и оприходования (черновики).
|
||
"""
|
||
|
||
@method_decorator(require_http_methods(["POST"]))
|
||
@transaction.atomic
|
||
def dispatch(self, *args, **kwargs):
|
||
return super().dispatch(*args, **kwargs)
|
||
|
||
def post(self, request, inventory_id):
|
||
try:
|
||
inventory = get_object_or_404(Inventory, id=inventory_id)
|
||
|
||
# Проверяем что инвентаризация не завершена
|
||
if inventory.status == 'completed':
|
||
messages.warning(request, 'Инвентаризация уже завершена.')
|
||
return redirect('inventory:inventory-detail', pk=inventory_id)
|
||
|
||
# Проверяем что есть строки
|
||
lines_count = InventoryLine.objects.filter(inventory=inventory).count()
|
||
if lines_count == 0:
|
||
messages.error(request, 'Нельзя завершить инвентаризацию без строк.')
|
||
return redirect('inventory:inventory-detail', pk=inventory_id)
|
||
|
||
# Обрабатываем инвентаризацию (создает документы-черновики)
|
||
result = InventoryProcessor.process_inventory(inventory_id)
|
||
|
||
# Автоматически проводим созданные документы
|
||
writeoff_confirmed = False
|
||
incoming_confirmed = False
|
||
confirmation_errors = []
|
||
|
||
if result['writeoff_document'] and result['writeoff_document'].status == 'draft':
|
||
try:
|
||
WriteOffDocumentService.confirm_document(
|
||
result['writeoff_document'],
|
||
confirmed_by=request.user
|
||
)
|
||
writeoff_confirmed = True
|
||
except ValidationError as e:
|
||
confirmation_errors.append(f'Ошибка проведения документа списания: {str(e)}')
|
||
|
||
if result['incoming_document'] and result['incoming_document'].status == 'draft':
|
||
try:
|
||
IncomingDocumentService.confirm_document(
|
||
result['incoming_document'],
|
||
confirmed_by=request.user
|
||
)
|
||
incoming_confirmed = True
|
||
except ValidationError as e:
|
||
confirmation_errors.append(f'Ошибка проведения документа оприходования: {str(e)}')
|
||
|
||
# Формируем сообщение
|
||
msg_parts = [f'Инвентаризация завершена. Обработано строк: {result["processed_lines"]}.']
|
||
|
||
if result['writeoff_document']:
|
||
if writeoff_confirmed:
|
||
msg_parts.append(
|
||
f'Документ списания {result["writeoff_document"].document_number} создан и автоматически проведен.'
|
||
)
|
||
else:
|
||
msg_parts.append(
|
||
f'Документ списания: {result["writeoff_document"].document_number}.'
|
||
)
|
||
|
||
if result['incoming_document']:
|
||
if incoming_confirmed:
|
||
msg_parts.append(
|
||
f'Документ оприходования {result["incoming_document"].document_number} создан и автоматически проведен.'
|
||
)
|
||
else:
|
||
msg_parts.append(
|
||
f'Документ оприходования: {result["incoming_document"].document_number}.'
|
||
)
|
||
|
||
if result['errors']:
|
||
msg_parts.append(f'Ошибок при обработке: {len(result["errors"])}.')
|
||
|
||
if confirmation_errors:
|
||
for error in confirmation_errors:
|
||
messages.error(request, error)
|
||
|
||
messages.success(request, ' '.join(msg_parts))
|
||
|
||
return redirect('inventory:inventory-detail', pk=inventory_id)
|
||
|
||
except Exception as e:
|
||
messages.error(request, f'Ошибка при завершении инвентаризации: {str(e)}')
|
||
return redirect('inventory:inventory-detail', pk=inventory_id)
|
||
|
||
|
||
class InventoryDeleteView(LoginRequiredMixin, View):
|
||
"""
|
||
Удаление инвентаризации.
|
||
Можно удалять только инвентаризации со статусом 'draft' или 'processing'.
|
||
Завершенные инвентаризации удалять нельзя, так как документы уже проведены.
|
||
"""
|
||
|
||
@method_decorator(require_http_methods(["POST"]))
|
||
@transaction.atomic
|
||
def dispatch(self, *args, **kwargs):
|
||
return super().dispatch(*args, **kwargs)
|
||
|
||
def post(self, request, pk):
|
||
try:
|
||
inventory = get_object_or_404(Inventory, pk=pk)
|
||
|
||
# Проверка статуса - завершенные нельзя удалять
|
||
if inventory.status == 'completed':
|
||
messages.error(
|
||
request,
|
||
'Нельзя удалить завершенную инвентаризацию. Документы уже проведены.'
|
||
)
|
||
return redirect('inventory:inventory-detail', pk=pk)
|
||
|
||
# Проверка связанных документов
|
||
writeoff_docs = inventory.writeoff_documents.all()
|
||
incoming_docs = inventory.incoming_documents.all()
|
||
|
||
# Проверяем, есть ли проведенные документы
|
||
if writeoff_docs.filter(status='confirmed').exists():
|
||
messages.error(
|
||
request,
|
||
'Нельзя удалить инвентаризацию с проведенными документами списания.'
|
||
)
|
||
return redirect('inventory:inventory-detail', pk=pk)
|
||
|
||
if incoming_docs.filter(status='confirmed').exists():
|
||
messages.error(
|
||
request,
|
||
'Нельзя удалить инвентаризацию с проведенными документами поступления.'
|
||
)
|
||
return redirect('inventory:inventory-detail', pk=pk)
|
||
|
||
# Сохраняем информацию для сообщения
|
||
warehouse_name = inventory.warehouse.name
|
||
document_number = inventory.document_number or f"#{inventory.id}"
|
||
|
||
# Удаляем документы-черновики вместе с инвентаризацией
|
||
writeoff_docs.filter(status='draft').delete()
|
||
incoming_docs.filter(status='draft').delete()
|
||
|
||
# Удаляем инвентаризацию (InventoryLine удалятся автоматически через CASCADE)
|
||
inventory.delete()
|
||
|
||
messages.success(
|
||
request,
|
||
f'Инвентаризация {document_number} склада "{warehouse_name}" успешно удалена.'
|
||
)
|
||
return redirect('inventory:inventory-list')
|
||
|
||
except Exception as e:
|
||
messages.error(request, f'Ошибка при удалении инвентаризации: {str(e)}')
|
||
return redirect('inventory:inventory-detail', pk=pk)
|
||
|
||
|