Добавлено сохранение snapshot-значений для проведенных инвентаризаций
- Добавлены поля snapshot_* в модель InventoryLine для фиксации значений на момент завершения - Обновлен InventoryProcessor для сохранения snapshot перед обработкой - Обновлен InventoryDetailView для отображения snapshot-значений в проведенных инвентаризациях - Добавлена миграция 0018 для новых полей - Теперь в проведенных инвентаризациях отображаются оригинальные значения и правильная разница, а не текущие скорректированные остатки
This commit is contained in:
@@ -52,18 +52,33 @@ class InventoryCreateView(LoginRequiredMixin, CreateView):
|
||||
model = Inventory
|
||||
form_class = InventoryForm
|
||||
template_name = 'inventory/inventory/inventory_form.html'
|
||||
success_url = reverse_lazy('inventory:inventory-list')
|
||||
|
||||
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):
|
||||
@@ -169,26 +184,52 @@ class InventoryDetailView(LoginRequiredMixin, DetailView):
|
||||
# Добавляем quantity_reserved и обновляем quantity_system для каждой строки
|
||||
lines_with_reserved = []
|
||||
for line in lines:
|
||||
stock = existing_stocks.get(line.product_id)
|
||||
if not stock:
|
||||
# Fallback на старый способ, если что-то пошло не так
|
||||
stock, _ = Stock.objects.get_or_create(
|
||||
product=line.product,
|
||||
warehouse=warehouse
|
||||
)
|
||||
stock.refresh_from_batches()
|
||||
|
||||
# Для незавершенных инвентаризаций обновляем quantity_system динамически
|
||||
if self.object.status != 'completed':
|
||||
# Для завершенных инвентаризаций используем 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
|
||||
|
||||
# Добавляем quantity_reserved и quantity_available
|
||||
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
|
||||
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)
|
||||
|
||||
@@ -556,3 +597,68 @@ class InventoryCompleteView(LoginRequiredMixin, View):
|
||||
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)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user