""" Inventory app views with healthcare-focused CRUD operations. """ from django.shortcuts import render, get_object_or_404, redirect from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib import messages from django.views.generic import ( ListView, DetailView, CreateView, UpdateView, DeleteView, TemplateView ) from django.urls import reverse_lazy, reverse from django.http import JsonResponse, HttpResponse from django.db.models import Q, Sum, Count, Avg, F from django.utils import timezone from django.core.paginator import Paginator from datetime import datetime, timedelta import csv from core.models import AuditLogEntry from .models import ( InventoryItem, InventoryStock, InventoryLocation, PurchaseOrder, PurchaseOrderItem, Supplier ) from .forms import ( InventoryItemForm, InventoryStockForm, InventoryLocationForm, PurchaseOrderForm, PurchaseOrderItemForm, SupplierForm ) class InventoryDashboardView(LoginRequiredMixin, TemplateView): """ Main inventory dashboard with comprehensive statistics and recent activity. """ template_name = 'inventory/dashboard.html' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) user = self.request.user # Basic inventory statistics context['total_items'] = InventoryItem.objects.filter(tenant=user.tenant).count() context['total_locations'] = InventoryLocation.objects.filter(tenant=user.tenant).count() context['total_suppliers'] = Supplier.objects.filter(tenant=user.tenant, is_active=True).count() context['active_orders'] = PurchaseOrder.objects.filter( tenant=user.tenant, status__in=['PENDING', 'APPROVED', 'ORDERED'] ).count() # Stock statistics low_stock_items = InventoryStock.objects.filter( inventory_item__tenant=user.tenant, quantity_available__lte=F('inventory_item__min_stock_level') ).count() context['low_stock_items'] = low_stock_items expired_items = InventoryStock.objects.filter( inventory_item__tenant=user.tenant, expiration_date__lte=timezone.now().date() ).count() context['expired_items'] = expired_items expiring_soon_items = InventoryStock.objects.filter( inventory_item__tenant=user.tenant, expiration_date__lte=timezone.now().date() + timedelta(days=30), expiration_date__gt=timezone.now().date() ).count() context['expiring_soon_items'] = expiring_soon_items # Financial statistics total_inventory_value = InventoryStock.objects.filter( inventory_item__tenant=user.tenant ).aggregate( total_value=Sum(F('quantity_available') * F('unit_cost')) )['total_value'] or 0 context['total_inventory_value'] = total_inventory_value # Recent activity context['recent_orders'] = PurchaseOrder.objects.filter( tenant=user.tenant ).order_by('-created_at')[:10] context['low_stock_alerts'] = InventoryStock.objects.filter( inventory_item__tenant=user.tenant, quantity_available__lte=F('inventory_item__min_stock_level') ).select_related('inventory_item', 'location').order_by('quantity_available')[:10] context['recent_stock_movements'] = InventoryStock.objects.filter( inventory_item__tenant=user.tenant ).order_by('-updated_at')[:10] return context class SupplierListView(LoginRequiredMixin, ListView): """ List all suppliers with search and filtering capabilities. """ model = Supplier template_name = 'inventory/suppliers/supplier_list.html' context_object_name = 'suppliers' paginate_by = 20 def get_queryset(self): queryset = Supplier.objects.filter(tenant=self.request.user.tenant) # Search functionality search = self.request.GET.get('search') if search: queryset = queryset.filter( Q(supplier_name__icontains=search) | Q(contact_person__icontains=search) | Q(email__icontains=search) | Q(phone__icontains=search) ) # Filter by status status = self.request.GET.get('status') if status: queryset = queryset.filter(is_active=(status == 'active')) # Filter by supplier type supplier_type = self.request.GET.get('supplier_type') if supplier_type: queryset = queryset.filter(supplier_type=supplier_type) return queryset.order_by('name') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['supplier_types'] = Supplier.SUPPLIER_TYPE_CHOICES return context class SupplierDetailView(LoginRequiredMixin, DetailView): """ Display detailed information about a supplier. """ model = Supplier template_name = 'inventory/suppliers/supplier_detail.html' context_object_name = 'supplier' def get_queryset(self): return Supplier.objects.filter(tenant=self.request.user.tenant) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) supplier = self.get_object() # Recent purchase orders from this supplier context['recent_orders'] = PurchaseOrder.objects.filter( tenant=self.request.user.tenant, supplier=supplier ).order_by('-order_date')[:10] # Supplier statistics context['total_orders'] = PurchaseOrder.objects.filter( tenant=self.request.user.tenant, supplier=supplier ).count() context['total_order_value'] = PurchaseOrder.objects.filter( tenant=self.request.user.tenant, supplier=supplier, status='RECEIVED' ).aggregate(total=Sum('total_amount'))['total'] or 0 return context class SupplierCreateView(LoginRequiredMixin, CreateView): """ Create a new supplier. """ model = Supplier form_class = SupplierForm template_name = 'inventory/suppliers/supplier_form.html' success_url = reverse_lazy('inventory:supplier_list') def form_valid(self, form): form.instance.tenant = self.request.user.tenant form.instance.created_by = self.request.user response = super().form_valid(form) # Log the creation AuditLogEntry.objects.create( tenant=self.request.user.tenant, user=self.request.user, action='CREATE', model_name='Supplier', object_id=self.object.pk, changes={'created': 'New supplier created'} ) messages.success(self.request, f'Supplier "{self.object.supplier_name}" created successfully.') return response class SupplierUpdateView(LoginRequiredMixin, UpdateView): """ Update an existing supplier. """ model = Supplier form_class = SupplierForm template_name = 'inventory/suppliers/supplier_form.html' def get_queryset(self): return Supplier.objects.filter(tenant=self.request.user.tenant) def get_success_url(self): return reverse('inventory:supplier_detail', kwargs={'pk': self.object.pk}) def form_valid(self, form): # Track changes for audit log old_values = {} new_values = {} for field in form.changed_data: old_values[field] = getattr(self.object, field) new_values[field] = form.cleaned_data[field] response = super().form_valid(form) # Log the update if form.changed_data: AuditLogEntry.objects.create( tenant=self.request.user.tenant, user=self.request.user, action='UPDATE', model_name='Supplier', object_id=self.object.pk, changes={'old': old_values, 'new': new_values} ) messages.success(self.request, f'Supplier "{self.object.supplier_name}" updated successfully.') return response class SupplierDeleteView(LoginRequiredMixin, DeleteView): """ Delete a supplier (soft delete by setting is_active=False). """ model = Supplier template_name = 'inventory/suppliers/supplier_confirm_delete.html' success_url = reverse_lazy('inventory:supplier_list') def get_queryset(self): return Supplier.objects.filter(tenant=self.request.user.tenant) def delete(self, request, *args, **kwargs): self.object = self.get_object() # Check if supplier has active orders active_orders = PurchaseOrder.objects.filter( tenant=request.user.tenant, supplier=self.object, status__in=['PENDING', 'APPROVED', 'ORDERED'] ).exists() if active_orders: messages.error(request, 'Cannot delete supplier with active purchase orders.') return redirect('inventory:supplier_detail', pk=self.object.pk) # Soft delete self.object.is_active = False self.object.save() # Log the deletion AuditLogEntry.objects.create( tenant=request.user.tenant, user=request.user, action='DELETE', model_name='Supplier', object_id=self.object.pk, changes={'deactivated': 'Supplier deactivated'} ) messages.success(request, f'Supplier "{self.object.supplier_name}" deactivated successfully.') return redirect(self.success_url) class InventoryLocationListView(LoginRequiredMixin, ListView): """ List all inventory locations. """ model = InventoryLocation template_name = 'inventory/locations/location_list.html' context_object_name = 'locations' paginate_by = 20 def get_queryset(self): queryset = InventoryLocation.objects.filter(tenant=self.request.user.tenant) # Search functionality search = self.request.GET.get('search') if search: queryset = queryset.filter( Q(location_name__icontains=search) | Q(location_code__icontains=search) | Q(description__icontains=search) ) # Filter by location type location_type = self.request.GET.get('location_type') if location_type: queryset = queryset.filter(location_type=location_type) return queryset.order_by('name') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['location_types'] = InventoryLocation.LOCATION_TYPE_CHOICES return context class InventoryLocationDetailView(LoginRequiredMixin, DetailView): """ Display detailed information about an inventory location. """ model = InventoryLocation template_name = 'inventory/locations/location_detail.html' context_object_name = 'location' def get_queryset(self): return InventoryLocation.objects.filter(tenant=self.request.user.tenant) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) location = self.get_object() # Stock items at this location context['stock_items'] = InventoryStock.objects.filter( inventory_item__tenant=self.request.user.tenant, location=location ).select_related('inventory_item').order_by('inventory_item__item_name') # Location statistics context['total_items'] = InventoryStock.objects.filter( inventory_item__tenant=self.request.user.tenant, location=location ).count() context['total_quantity'] = InventoryStock.objects.filter( inventory_item__tenant=self.request.user.tenant, location=location ).aggregate(total=Sum('quantity_available'))['total'] or 0 context['total_value'] = InventoryStock.objects.filter( inventory_item__tenant=self.request.user.tenant, location=location ).aggregate( total_value=Sum(F('quantity_available') * F('unit_cost')) )['total_value'] or 0 return context class InventoryLocationCreateView(LoginRequiredMixin, CreateView): """ Create a new inventory location. """ model = InventoryLocation form_class = InventoryLocationForm template_name = 'inventory/locations/location_form.html' success_url = reverse_lazy('inventory:location_list') def form_valid(self, form): form.instance.tenant = self.request.user.tenant response = super().form_valid(form) # Log the creation AuditLogEntry.objects.create( tenant=self.request.user.tenant, user=self.request.user, action='CREATE', model_name='InventoryLocation', object_id=self.object.pk, changes={'created': 'New inventory location created'} ) messages.success(self.request, f'Location "{self.object.location_name}" created successfully.') return response class InventoryLocationUpdateView(LoginRequiredMixin, UpdateView): """ Update an existing inventory location. """ model = InventoryLocation form_class = InventoryLocationForm template_name = 'inventory/locations/location_form.html' def get_queryset(self): return InventoryLocation.objects.filter(tenant=self.request.user.tenant) def get_success_url(self): return reverse('inventory:location_detail', kwargs={'pk': self.object.pk}) def form_valid(self, form): response = super().form_valid(form) messages.success(self.request, f'Location "{self.object.location_name}" updated successfully.') return response class InventoryLocationDeleteView(LoginRequiredMixin, DeleteView): """ Delete an inventory location. """ model = InventoryLocation template_name = 'inventory/locations/location_confirm_delete.html' success_url = reverse_lazy('inventory:location_list') def get_queryset(self): return InventoryLocation.objects.filter(tenant=self.request.user.tenant) def delete(self, request, *args, **kwargs): self.object = self.get_object() # Check if location has inventory has_inventory = InventoryStock.objects.filter( tenant=request.user.tenant, location=self.object ).exists() if has_inventory: messages.error(request, 'Cannot delete location with existing inventory.') return redirect('inventory:location_detail', pk=self.object.pk) response = super().delete(request, *args, **kwargs) messages.success(request, f'Location "{self.object.location_name}" deleted successfully.') return response class InventoryItemListView(LoginRequiredMixin, ListView): """ List all inventory items with search and filtering. """ model = InventoryItem template_name = 'inventory/items/item_list.html' context_object_name = 'items' paginate_by = 20 def get_queryset(self): queryset = InventoryItem.objects.filter(tenant=self.request.user.tenant) # Search functionality search = self.request.GET.get('search') if search: queryset = queryset.filter( Q(item_name__icontains=search) | Q(item_code__icontains=search) | Q(description__icontains=search) | Q(manufacturer__icontains=search) ) # Filter by category category = self.request.GET.get('category') if category: queryset = queryset.filter(category=category) # Filter by status status = self.request.GET.get('status') if status: queryset = queryset.filter(is_active=(status == 'active')) return queryset.order_by('item_name') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['categories'] = InventoryItem.CATEGORY_CHOICES return context class InventoryItemDetailView(LoginRequiredMixin, DetailView): """ Display detailed information about an inventory item. """ model = InventoryItem template_name = 'inventory/items/item_detail.html' context_object_name = 'item' def get_queryset(self): return InventoryItem.objects.filter(tenant=self.request.user.tenant) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) item = self.get_object() # Stock information across all locations context['stock_locations'] = InventoryStock.objects.filter( inventory_item__tenant=self.request.user.tenant, inventory_item=item ).select_related('location').order_by('location__location_name') # Item statistics context['total_stock'] = InventoryStock.objects.filter( inventory_item__tenant=self.request.user.tenant, inventory_item=item ).aggregate(total=Sum('quantity_available'))['total'] or 0 context['total_value'] = InventoryStock.objects.filter( inventory_item__tenant=self.request.user.tenant, inventory_item=item ).aggregate( total_value=Sum(F('quantity_available') * F('unit_cost')) )['total_value'] or 0 # Recent purchase orders for this item context['recent_orders'] = PurchaseOrderItem.objects.filter( purchase_order__tenant=self.request.user.tenant, inventory_item=item ).select_related('purchase_order').order_by('-purchase_order__order_date')[:5] return context class InventoryItemCreateView(LoginRequiredMixin, CreateView): """ Create a new inventory item. """ model = InventoryItem form_class = InventoryItemForm template_name = 'inventory/items/item_form.html' success_url = reverse_lazy('inventory:item_list') def form_valid(self, form): form.instance.tenant = self.request.user.tenant response = super().form_valid(form) # Log the creation AuditLogEntry.objects.create( tenant=self.request.user.tenant, user=self.request.user, action='CREATE', model_name='InventoryItem', object_id=self.object.pk, changes={'created': 'New inventory item created'} ) messages.success(self.request, f'Item "{self.object.item_name}" created successfully.') return response class InventoryItemUpdateView(LoginRequiredMixin, UpdateView): """ Update an existing inventory item. """ model = InventoryItem form_class = InventoryItemForm template_name = 'inventory/items/item_form.html' def get_queryset(self): return InventoryItem.objects.filter(tenant=self.request.user.tenant) def get_success_url(self): return reverse('inventory:item_detail', kwargs={'pk': self.object.pk}) def form_valid(self, form): response = super().form_valid(form) messages.success(self.request, f'Item "{self.object.item_name}" updated successfully.') return response class InventoryItemDeleteView(LoginRequiredMixin, DeleteView): """ Delete an inventory item (soft delete). """ model = InventoryItem template_name = 'inventory/items/item_confirm_delete.html' success_url = reverse_lazy('inventory:item_list') def get_queryset(self): return InventoryItem.objects.filter(tenant=self.request.user.tenant) def delete(self, request, *args, **kwargs): self.object = self.get_object() # Check if item has stock has_stock = InventoryStock.objects.filter( tenant=request.user.tenant, item=self.object, quantity__gt=0 ).exists() if has_stock: messages.error(request, 'Cannot delete item with existing stock.') return redirect('inventory:item_detail', pk=self.object.pk) # Soft delete self.object.is_active = False self.object.save() messages.success(request, f'Item "{self.object.item_name}" deactivated successfully.') return redirect(self.success_url) class InventoryStockListView(LoginRequiredMixin, ListView): """ List all inventory stock with filtering and search. """ model = InventoryStock template_name = 'inventory/stock/stock_list.html' context_object_name = 'stock_items' paginate_by = 20 def get_queryset(self): queryset = InventoryStock.objects.filter( inventory_item__tenant=self.request.user.tenant ).select_related('inventory_item', 'location') # Search functionality search = self.request.GET.get('search') if search: queryset = queryset.filter( Q(inventory_item__item_name__icontains=search) | Q(inventory_item__item_code__icontains=search) | Q(location__location_name__icontains=search) ) # Filter by location location = self.request.GET.get('location') if location: queryset = queryset.filter(location_id=location) # Filter by stock status stock_status = self.request.GET.get('stock_status') if stock_status == 'low': queryset = queryset.filter(quantity_available__lte=F('quantity_available')) elif stock_status == 'out': queryset = queryset.filter(quantity=0) elif stock_status == 'expired': queryset = queryset.filter(expiry_date__lte=timezone.now().date()) elif stock_status == 'expiring': queryset = queryset.filter( expiry_date__lte=timezone.now().date() + timedelta(days=30), expiry_date__gt=timezone.now().date() ) return queryset.order_by('inventory_item__item_name', 'location__name') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['locations'] = InventoryLocation.objects.filter( tenant=self.request.user.tenant ).order_by('name') return context class InventoryStockDetailView(LoginRequiredMixin, DetailView): """ Display detailed information about inventory stock. """ model = InventoryStock template_name = 'inventory/stock/stock_detail.html' context_object_name = 'stock' def get_queryset(self): return InventoryStock.objects.filter( inventory_item__tenant=self.request.user.tenant ).select_related('inventory_item', 'location') class InventoryStockCreateView(LoginRequiredMixin, CreateView): """ Create new inventory stock entry. """ model = InventoryStock form_class = InventoryStockForm template_name = 'inventory/stock/stock_form.html' success_url = reverse_lazy('inventory:stock_list') def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user return kwargs def form_valid(self, form): form.instance.tenant = self.request.user.tenant response = super().form_valid(form) # Log the creation AuditLogEntry.objects.create( tenant=self.request.user.tenant, user=self.request.user, action='CREATE', model_name='InventoryStock', object_id=self.object.pk, changes={'created': 'New stock entry created'} ) messages.success(self.request, 'Stock entry created successfully.') return response class InventoryStockUpdateView(LoginRequiredMixin, UpdateView): """ Update inventory stock (limited fields for operational adjustments). """ model = InventoryStock form_class = InventoryStockForm template_name = 'inventory/stock/stock_form.html' def get_queryset(self): return InventoryStock.objects.filter(inventory_item__tenant=self.request.user.tenant) def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user return kwargs def get_success_url(self): return reverse('inventory:stock_detail', kwargs={'pk': self.object.pk}) def form_valid(self, form): response = super().form_valid(form) messages.success(self.request, 'Stock updated successfully.') return response class PurchaseOrderListView(LoginRequiredMixin, ListView): """ List all purchase orders with filtering. """ model = PurchaseOrder template_name = 'inventory/orders/purchase_order_list.html' context_object_name = 'orders' paginate_by = 20 def get_queryset(self): queryset = PurchaseOrder.objects.filter( tenant=self.request.user.tenant ).select_related('supplier') # Search functionality search = self.request.GET.get('search') if search: queryset = queryset.filter( Q(order_number__icontains=search) | Q(supplier__supplier_name__icontains=search) ) # Filter by status status = self.request.GET.get('status') if status: queryset = queryset.filter(status=status) # Filter by supplier supplier = self.request.GET.get('supplier') if supplier: queryset = queryset.filter(supplier_id=supplier) return queryset.order_by('-order_date') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['statuses'] = PurchaseOrder.STATUS_CHOICES context['suppliers'] = Supplier.objects.filter( tenant=self.request.user.tenant, is_active=True ).order_by('name') return context class PurchaseOrderDetailView(LoginRequiredMixin, DetailView): """ Display detailed information about a purchase order. """ model = PurchaseOrder template_name = 'inventory/orders/purchase_order_detail.html' context_object_name = 'order' def get_queryset(self): return PurchaseOrder.objects.filter( tenant=self.request.user.tenant ).select_related('supplier') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) order = self.get_object() # Order items context['order_items'] = PurchaseOrderItem.objects.filter( purchase_order=order ).select_related('inventory_item').order_by('inventory_item__item_name') return context class PurchaseOrderCreateView(LoginRequiredMixin, CreateView): """ Create a new purchase order. """ model = PurchaseOrder form_class = PurchaseOrderForm template_name = 'inventory/orders/purchase_order_form.html' success_url = reverse_lazy('inventory:purchase_order_list') def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user return kwargs def form_valid(self, form): form.instance.tenant = self.request.user.tenant form.instance.created_by = self.request.user response = super().form_valid(form) # Log the creation AuditLogEntry.objects.create( tenant=self.request.user.tenant, user=self.request.user, action='CREATE', model_name='PurchaseOrder', object_id=self.object.pk, changes={'created': 'New purchase order created'} ) messages.success(self.request, f'Purchase Order "{self.object.order_number}" created successfully.') return response class PurchaseOrderUpdateView(LoginRequiredMixin, UpdateView): """ Update a purchase order (limited updates based on status). """ model = PurchaseOrder form_class = PurchaseOrderForm template_name = 'inventory/orders/purchase_order_form.html' def get_queryset(self): return PurchaseOrder.objects.filter(tenant=self.request.user.tenant) def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user return kwargs def get_success_url(self): return reverse('inventory:purchase_order_detail', kwargs={'pk': self.object.pk}) def form_valid(self, form): # Check if order can be updated if self.object.status in ['RECEIVED', 'CANCELLED']: messages.error(self.request, 'Cannot update completed or cancelled orders.') return redirect('inventory:purchase_order_detail', pk=self.object.pk) response = super().form_valid(form) messages.success(self.request, f'Purchase Order "{self.object.order_number}" updated successfully.') return response @login_required def inventory_stats(request): """ HTMX endpoint for real-time inventory statistics. """ user = request.user # Calculate statistics stats = { 'total_items': InventoryItem.objects.filter(tenant=user.tenant).count(), 'low_stock_items': InventoryStock.objects.filter( inventory_item__tenant=user.tenant, quantity_available__lte=F('quantity_available') ).count(), 'expired_items': InventoryStock.objects.filter( inventory_item__tenant=user.tenant, expiration_date__lte=timezone.now().date() ).count(), 'active_orders': PurchaseOrder.objects.filter( tenant=user.tenant, status__in=['PENDING', 'APPROVED', 'ORDERED'] ).count(), 'recent_stock_movements': InventoryStock.objects.filter( inventory_item__tenant=user.tenant, movement_date__gte=timezone.now().date() - timedelta(days=30) ) } return render(request, 'inventory/partials/inventory_stats.html', stats) @login_required def stock_search(request): """ HTMX endpoint for dynamic stock search. """ search = request.GET.get('search', '') stock_items = InventoryStock.objects.filter( tenant=request.user.tenant, item__item_name__icontains=search ).select_related('item', 'location')[:10] return render(request, 'inventory/partials/stock_search_results.html', { 'stock_items': stock_items }) @login_required def adjust_stock(request, stock_id): """ Adjust inventory stock quantity. """ stock = get_object_or_404( InventoryStock, pk=stock_id, tenant=request.user.tenant ) if request.method == 'POST': adjustment = request.POST.get('adjustment', 0) reason = request.POST.get('reason', '') try: adjustment = int(adjustment) old_quantity = stock.quantity stock.quantity = max(0, stock.quantity + adjustment) stock.save() # Log the adjustment AuditLogEntry.objects.create( tenant=request.user.tenant, user=request.user, action='UPDATE', model_name='InventoryStock', object_id=stock.pk, changes={ 'adjustment': adjustment, 'old_quantity': old_quantity, 'new_quantity': stock.quantity, 'reason': reason } ) messages.success(request, f'Stock adjusted successfully. New quantity: {stock.quantity}') except ValueError: messages.error(request, 'Invalid adjustment value.') return redirect('inventory:stock_detail', pk=stock.pk) @login_required def approve_purchase_order(request, order_id): """ Approve a purchase order. """ order = get_object_or_404( PurchaseOrder, pk=order_id, tenant=request.user.tenant ) if order.status == 'PENDING': order.status = 'APPROVED' order.approved_by = request.user order.approved_date = timezone.now() order.save() # Log the approval AuditLogEntry.objects.create( tenant=request.user.tenant, user=request.user, action='UPDATE', model_name='PurchaseOrder', object_id=order.pk, changes={'status': 'APPROVED', 'approved_by': request.user.username} ) messages.success(request, f'Purchase Order "{order.order_number}" approved successfully.') else: messages.error(request, 'Order cannot be approved in its current status.') return redirect('inventory:purchase_order_detail', pk=order.pk) @login_required def receive_purchase_order(request, order_id): """ Mark a purchase order as received and update stock. """ order = get_object_or_404( PurchaseOrder, pk=order_id, tenant=request.user.tenant ) if order.status == 'ORDERED': order.status = 'RECEIVED' order.received_date = timezone.now() order.save() # Update stock for each item for item in order.purchaseorderitem_set.all(): stock, created = InventoryStock.objects.get_or_create( tenant=request.user.tenant, item=item.item, location=order.delivery_location, defaults={ 'quantity': 0, 'unit_cost': item.unit_price, 'minimum_stock_level': 10 } ) stock.quantity += item.quantity stock.unit_cost = item.unit_price # Update with latest cost stock.save() # Log the receipt AuditLogEntry.objects.create( tenant=request.user.tenant, user=request.user, action='UPDATE', model_name='PurchaseOrder', object_id=order.pk, changes={'status': 'RECEIVED', 'received_date': timezone.now().isoformat()} ) messages.success(request, f'Purchase Order "{order.order_number}" received and stock updated.') else: messages.error(request, 'Order cannot be received in its current status.') return redirect('inventory:purchase_order_detail', pk=order.pk) # # """ # Inventory app views with healthcare-focused CRUD operations. # """ # # from django.shortcuts import render, get_object_or_404, redirect # from django.contrib.auth.decorators import login_required # from django.contrib.auth.mixins import LoginRequiredMixin # from django.contrib import messages # from django.views.generic import ( # ListView, DetailView, CreateView, UpdateView, DeleteView, TemplateView # ) # from django.urls import reverse_lazy, reverse # from django.http import JsonResponse, HttpResponse # from django.db.models import Q, Sum, Count, Avg, F # from django.utils import timezone # from django.core.paginator import Paginator # from datetime import datetime, timedelta # import csv # # from core.models import AuditLogEntry # from .models import ( # InventoryItem, InventoryStock, InventoryLocation, # PurchaseOrder, PurchaseOrderItem, Supplier # ) # from .forms import ( # InventoryItemForm, InventoryStockForm, InventoryLocationForm, # PurchaseOrderForm, PurchaseOrderItemForm, SupplierForm # ) # # # # ============================================================================ # # DASHBOARD AND OVERVIEW VIEWS # # ============================================================================ # # class InventoryDashboardView(LoginRequiredMixin, TemplateView): # """ # Main inventory dashboard with comprehensive statistics and recent activity. # """ # template_name = 'inventory/dashboard.html' # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # user = self.request.user # # # Basic inventory statistics # context['total_items'] = InventoryItem.objects.filter(tenant=user.tenant).count() # context['total_locations'] = InventoryLocation.objects.filter(tenant=user.tenant).count() # context['total_suppliers'] = Supplier.objects.filter(tenant=user.tenant, is_active=True).count() # context['active_orders'] = PurchaseOrder.objects.filter( # tenant=user.tenant, # status__in=['PENDING', 'APPROVED', 'ORDERED'] # ).count() # # # Stock statistics # # low_stock_items = InventoryStock.objects.filter( # # tenant=user.tenant, # # quantity_available__lte=F('minimum_stock_level') # # ).count() # # context['low_stock_items'] = low_stock_items # # expired_items = InventoryStock.objects.filter( # inventory_item__tenant=user.tenant, # expiration_date__lte=timezone.now().date() # ).count() # context['expired_items'] = expired_items # # expiring_soon_items = InventoryStock.objects.filter( # inventory_item__tenant=user.tenant, # expiration_date__lte=timezone.now().date() + timedelta(days=30), # expiration_date__gt=timezone.now().date() # ).count() # context['expiring_soon_items'] = expiring_soon_items # # # Financial statistics # total_inventory_value = InventoryStock.objects.filter( # inventory_item__tenant=user.tenant # ).aggregate( # total_value=Sum(F('quantity_on_hand') * F('unit_cost')) # )['total_value'] or 0 # context['total_inventory_value'] = total_inventory_value # # # Recent activity # context['recent_orders'] = PurchaseOrder.objects.filter( # tenant=user.tenant # ).order_by('-created_at')[:10] # # # context['low_stock_alerts'] = InventoryStock.objects.filter( # # tenant=user.tenant, # # quantity_available__lte=F('minimum_stock_level') # # ).select_related('item', 'location') # # context['recent_stock_movements'] = InventoryStock.objects.filter( # inventory_item__tenant=user.tenant # ).order_by('-updated_at')[:10] # # return context # # # # ============================================================================ # # SUPPLIER VIEWS (FULL CRUD - Master Data) # # ============================================================================ # # class SupplierListView(LoginRequiredMixin, ListView): # """ # List all suppliers with search and filtering capabilities. # """ # model = Supplier # template_name = 'inventory/supplier_list.html' # context_object_name = 'suppliers' # paginate_by = 20 # # def get_queryset(self): # queryset = Supplier.objects.filter(tenant=self.request.user.tenant) # # # Search functionality # search = self.request.GET.get('search') # if search: # queryset = queryset.filter( # Q(supplier_name__icontains=search) | # Q(contact_person__icontains=search) | # Q(email__icontains=search) | # Q(phone__icontains=search) # ) # # # Filter by status # status = self.request.GET.get('status') # if status: # queryset = queryset.filter(is_active=(status == 'active')) # # # Filter by supplier type # supplier_type = self.request.GET.get('supplier_type') # if supplier_type: # queryset = queryset.filter(supplier_type=supplier_type) # # return queryset.order_by('supplier_name') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context['supplier_types'] = Supplier.SUPPLIER_TYPE_CHOICES # return context # # # class SupplierDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about a supplier. # """ # model = Supplier # template_name = 'inventory/supplier_detail.html' # context_object_name = 'supplier' # # def get_queryset(self): # return Supplier.objects.filter(tenant=self.request.user.tenant) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # supplier = self.get_object() # # # Recent purchase orders from this supplier # context['recent_orders'] = PurchaseOrder.objects.filter( # tenant=self.request.user.tenant, # supplier=supplier # ).order_by('-order_date')[:10] # # # Supplier statistics # context['total_orders'] = PurchaseOrder.objects.filter( # tenant=self.request.user.tenant, # supplier=supplier # ).count() # # context['total_order_value'] = PurchaseOrder.objects.filter( # tenant=self.request.user.tenant, # supplier=supplier, # status='RECEIVED' # ).aggregate(total=Sum('total_amount'))['total'] or 0 # # return context # # # class SupplierCreateView(LoginRequiredMixin, CreateView): # """ # Create a new supplier. # """ # model = Supplier # form_class = SupplierForm # template_name = 'inventory/supplier_form.html' # success_url = reverse_lazy('inventory:supplier_list') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # form.instance.created_by = self.request.user # response = super().form_valid(form) # # # Log the creation # AuditLogEntry.objects.create( # tenant=self.request.user.tenant, # user=self.request.user, # action='CREATE', # model_name='Supplier', # object_id=self.object.pk, # changes={'created': 'New supplier created'} # ) # # messages.success(self.request, f'Supplier "{self.object.supplier_name}" created successfully.') # return response # # # class SupplierUpdateView(LoginRequiredMixin, UpdateView): # """ # Update an existing supplier. # """ # model = Supplier # form_class = SupplierForm # template_name = 'inventory/supplier_form.html' # # def get_queryset(self): # return Supplier.objects.filter(tenant=self.request.user.tenant) # # def get_success_url(self): # return reverse('inventory:supplier_detail', kwargs={'pk': self.object.pk}) # # def form_valid(self, form): # # Track changes for audit log # old_values = {} # new_values = {} # for field in form.changed_data: # old_values[field] = getattr(self.object, field) # new_values[field] = form.cleaned_data[field] # # response = super().form_valid(form) # # # Log the update # if form.changed_data: # AuditLogEntry.objects.create( # tenant=self.request.user.tenant, # user=self.request.user, # action='UPDATE', # model_name='Supplier', # object_id=self.object.pk, # changes={'old': old_values, 'new': new_values} # ) # # messages.success(self.request, f'Supplier "{self.object.supplier_name}" updated successfully.') # return response # # # class SupplierDeleteView(LoginRequiredMixin, DeleteView): # """ # Delete a supplier (soft delete by setting is_active=False). # """ # model = Supplier # template_name = 'inventory/supplier_confirm_delete.html' # success_url = reverse_lazy('inventory:supplier_list') # # def get_queryset(self): # return Supplier.objects.filter(tenant=self.request.user.tenant) # # def delete(self, request, *args, **kwargs): # self.object = self.get_object() # # # Check if supplier has active orders # active_orders = PurchaseOrder.objects.filter( # tenant=request.user.tenant, # supplier=self.object, # status__in=['PENDING', 'APPROVED', 'ORDERED'] # ).exists() # # if active_orders: # messages.error(request, 'Cannot delete supplier with active purchase orders.') # return redirect('inventory:supplier_detail', pk=self.object.pk) # # # Soft delete # self.object.is_active = False # self.object.save() # # # Log the deletion # AuditLogEntry.objects.create( # tenant=request.user.tenant, # user=request.user, # action='DELETE', # model_name='Supplier', # object_id=self.object.pk, # changes={'deactivated': 'Supplier deactivated'} # ) # # messages.success(request, f'Supplier "{self.object.supplier_name}" deactivated successfully.') # return redirect(self.success_url) # # # # ============================================================================ # # INVENTORY LOCATION VIEWS (FULL CRUD - Master Data) # # ============================================================================ # # class InventoryLocationListView(LoginRequiredMixin, ListView): # """ # List all inventory locations. # """ # model = InventoryLocation # template_name = 'inventory/location_list.html' # context_object_name = 'locations' # paginate_by = 20 # # def get_queryset(self): # queryset = InventoryLocation.objects.filter(tenant=self.request.user.tenant) # # # Search functionality # search = self.request.GET.get('search') # if search: # queryset = queryset.filter( # Q(location_name__icontains=search) | # Q(location_code__icontains=search) | # Q(description__icontains=search) # ) # # # Filter by location type # location_type = self.request.GET.get('location_type') # if location_type: # queryset = queryset.filter(location_type=location_type) # # return queryset.order_by('location_name') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context['location_types'] = InventoryLocation.LOCATION_TYPE_CHOICES # return context # # # class InventoryLocationDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about an inventory location. # """ # model = InventoryLocation # template_name = 'inventory/location_detail.html' # context_object_name = 'location' # # def get_queryset(self): # return InventoryLocation.objects.filter(tenant=self.request.user.tenant) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # location = self.get_object() # # # Stock items at this location # context['stock_items'] = InventoryStock.objects.filter( # tenant=self.request.user.tenant, # location=location # ).select_related('item').order_by('item__item_name') # # # Location statistics # context['total_items'] = InventoryStock.objects.filter( # tenant=self.request.user.tenant, # location=location # ).count() # # context['total_quantity'] = InventoryStock.objects.filter( # tenant=self.request.user.tenant, # location=location # ).aggregate(total=Sum('quantity'))['total'] or 0 # # context['total_value'] = InventoryStock.objects.filter( # tenant=self.request.user.tenant, # location=location # ).aggregate( # total_value=Sum(F('quantity') * F('unit_cost')) # )['total_value'] or 0 # # return context # # # class InventoryLocationCreateView(LoginRequiredMixin, CreateView): # """ # Create a new inventory location. # """ # model = InventoryLocation # form_class = InventoryLocationForm # template_name = 'inventory/location_form.html' # success_url = reverse_lazy('inventory:location_list') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # response = super().form_valid(form) # # # Log the creation # AuditLogEntry.objects.create( # tenant=self.request.user.tenant, # user=self.request.user, # action='CREATE', # model_name='InventoryLocation', # object_id=self.object.pk, # changes={'created': 'New inventory location created'} # ) # # messages.success(self.request, f'Location "{self.object.location_name}" created successfully.') # return response # # # class InventoryLocationUpdateView(LoginRequiredMixin, UpdateView): # """ # Update an existing inventory location. # """ # model = InventoryLocation # form_class = InventoryLocationForm # template_name = 'inventory/location_form.html' # # def get_queryset(self): # return InventoryLocation.objects.filter(tenant=self.request.user.tenant) # # def get_success_url(self): # return reverse('inventory:location_detail', kwargs={'pk': self.object.pk}) # # def form_valid(self, form): # response = super().form_valid(form) # messages.success(self.request, f'Location "{self.object.location_name}" updated successfully.') # return response # # # class InventoryLocationDeleteView(LoginRequiredMixin, DeleteView): # """ # Delete an inventory location. # """ # model = InventoryLocation # template_name = 'inventory/location_confirm_delete.html' # success_url = reverse_lazy('inventory:location_list') # # def get_queryset(self): # return InventoryLocation.objects.filter(tenant=self.request.user.tenant) # # def delete(self, request, *args, **kwargs): # self.object = self.get_object() # # # Check if location has inventory # has_inventory = InventoryStock.objects.filter( # tenant=request.user.tenant, # location=self.object # ).exists() # # if has_inventory: # messages.error(request, 'Cannot delete location with existing inventory.') # return redirect('inventory:location_detail', pk=self.object.pk) # # response = super().delete(request, *args, **kwargs) # messages.success(request, f'Location "{self.object.location_name}" deleted successfully.') # return response # # # # ============================================================================ # # INVENTORY ITEM VIEWS (FULL CRUD - Master Data) # # ============================================================================ # # class InventoryItemListView(LoginRequiredMixin, ListView): # """ # List all inventory items with search and filtering. # """ # model = InventoryItem # template_name = 'inventory/item_list.html' # context_object_name = 'items' # paginate_by = 20 # # def get_queryset(self): # queryset = InventoryItem.objects.filter(tenant=self.request.user.tenant) # # # Search functionality # search = self.request.GET.get('search') # if search: # queryset = queryset.filter( # Q(item_name__icontains=search) | # Q(item_code__icontains=search) | # Q(description__icontains=search) | # Q(manufacturer__icontains=search) # ) # # # Filter by category # category = self.request.GET.get('category') # if category: # queryset = queryset.filter(category=category) # # # Filter by status # status = self.request.GET.get('status') # if status: # queryset = queryset.filter(is_active=(status == 'active')) # # return queryset.order_by('item_name') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context['categories'] = InventoryItem.CATEGORY_CHOICES # return context # # # class InventoryItemDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about an inventory item. # """ # model = InventoryItem # template_name = 'inventory/item_detail.html' # context_object_name = 'item' # # def get_queryset(self): # return InventoryItem.objects.filter(tenant=self.request.user.tenant) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # item = self.get_object() # # # Stock information across all locations # context['stock_locations'] = InventoryStock.objects.filter( # inventory_item_tenant=self.request.user.tenant, # item=item # ).select_related('location').order_by('location__location_name') # # # Item statistics # context['total_stock'] = InventoryStock.objects.filter( # inventory_item_tenant=self.request.user.tenant, # item=item # ).aggregate(total=Sum('quantity_available'))['total'] or 0 # # context['total_value'] = InventoryStock.objects.filter( # inventory_item_tenant=self.request.user.tenant, # item=item # ).aggregate( # total_value=Sum(F('quantity_available') * F('unit_cost')) # )['total_value'] or 0 # # # Recent purchase orders for this item # context['recent_orders'] = PurchaseOrderItem.objects.filter( # purchase_order__tenant=self.request.user.tenant, # item=item # ).select_related('purchase_order').order_by('-purchase_order__order_date')[:5] # # return context # # # class InventoryItemCreateView(LoginRequiredMixin, CreateView): # """ # Create a new inventory item. # """ # model = InventoryItem # form_class = InventoryItemForm # template_name = 'inventory/item_form.html' # success_url = reverse_lazy('inventory:item_list') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # response = super().form_valid(form) # # # Log the creation # AuditLogEntry.objects.create( # tenant=self.request.user.tenant, # user=self.request.user, # action='CREATE', # model_name='InventoryItem', # object_id=self.object.pk, # changes={'created': 'New inventory item created'} # ) # # messages.success(self.request, f'Item "{self.object.item_name}" created successfully.') # return response # # # class InventoryItemUpdateView(LoginRequiredMixin, UpdateView): # """ # Update an existing inventory item. # """ # model = InventoryItem # form_class = InventoryItemForm # template_name = 'inventory/item_form.html' # # def get_queryset(self): # return InventoryItem.objects.filter(tenant=self.request.user.tenant) # # def get_success_url(self): # return reverse('inventory:item_detail', kwargs={'pk': self.object.pk}) # # def form_valid(self, form): # response = super().form_valid(form) # messages.success(self.request, f'Item "{self.object.item_name}" updated successfully.') # return response # # # class InventoryItemDeleteView(LoginRequiredMixin, DeleteView): # """ # Delete an inventory item (soft delete). # """ # model = InventoryItem # template_name = 'inventory/item_confirm_delete.html' # success_url = reverse_lazy('inventory:item_list') # # def get_queryset(self): # return InventoryItem.objects.filter(tenant=self.request.user.tenant) # # def delete(self, request, *args, **kwargs): # self.object = self.get_object() # # # Check if item has stock # has_stock = InventoryStock.objects.filter( # tenant=request.user.tenant, # item=self.object, # quantity__gt=0 # ).exists() # # if has_stock: # messages.error(request, 'Cannot delete item with existing stock.') # return redirect('inventory:item_detail', pk=self.object.pk) # # # Soft delete # self.object.is_active = False # self.object.save() # # messages.success(request, f'Item "{self.object.item_name}" deactivated successfully.') # return redirect(self.success_url) # # # # ============================================================================ # # INVENTORY STOCK VIEWS (LIMITED CRUD - Operational Data) # # ============================================================================ # # class InventoryStockListView(LoginRequiredMixin, ListView): # """ # List all inventory stock with filtering and search. # """ # model = InventoryStock # template_name = 'inventory/stock_list.html' # context_object_name = 'stock_items' # paginate_by = 20 # # def get_queryset(self): # queryset = InventoryStock.objects.filter( # inventory_item__tenant=self.request.user.tenant # ).select_related('inventory_item', 'location') # # # Search functionality # search = self.request.GET.get('search') # if search: # queryset = queryset.filter( # Q(item__item_name__icontains=search) | # Q(item__item_code__icontains=search) | # Q(location__location_name__icontains=search) # ) # # # Filter by location # location = self.request.GET.get('location') # if location: # queryset = queryset.filter(location_id=location) # # # Filter by stock status # stock_status = self.request.GET.get('stock_status') # if stock_status == 'low': # queryset = queryset.filter(quantity_available__lte=F('minimum_stock_level')) # elif stock_status == 'out': # queryset = queryset.filter(quantity_available=0) # elif stock_status == 'expired': # queryset = queryset.filter(expiry_date__lte=timezone.now().date()) # elif stock_status == 'expiring': # queryset = queryset.filter( # expiry_date__lte=timezone.now().date() + timedelta(days=30), # expiry_date__gt=timezone.now().date() # ) # # return queryset.order_by('inventory_item__item_name', 'location__location_name') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context['locations'] = InventoryLocation.objects.filter( # tenant=self.request.user.tenant # ) # return context # # # class InventoryStockDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about inventory stock. # """ # model = InventoryStock # template_name = 'inventory/stock_detail.html' # context_object_name = 'stock' # # def get_queryset(self): # return InventoryStock.objects.filter( # tenant=self.request.user.tenant # ).select_related('item', 'location') # # # class InventoryStockCreateView(LoginRequiredMixin, CreateView): # """ # Create new inventory stock entry. # """ # model = InventoryStock # form_class = InventoryStockForm # template_name = 'inventory/stock_form.html' # success_url = reverse_lazy('inventory:stock_list') # # def get_form_kwargs(self): # kwargs = super().get_form_kwargs() # kwargs['user'] = self.request.user # return kwargs # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # response = super().form_valid(form) # # # Log the creation # AuditLogEntry.objects.create( # tenant=self.request.user.tenant, # user=self.request.user, # action='CREATE', # model_name='InventoryStock', # object_id=self.object.pk, # changes={'created': 'New stock entry created'} # ) # # messages.success(self.request, 'Stock entry created successfully.') # return response # # # class InventoryStockUpdateView(LoginRequiredMixin, UpdateView): # """ # Update inventory stock (limited fields for operational adjustments). # """ # model = InventoryStock # form_class = InventoryStockForm # template_name = 'inventory/stock_form.html' # # def get_queryset(self): # return InventoryStock.objects.filter(tenant=self.request.user.tenant) # # def get_form_kwargs(self): # kwargs = super().get_form_kwargs() # kwargs['user'] = self.request.user # return kwargs # # def get_success_url(self): # return reverse('inventory:stock_detail', kwargs={'pk': self.object.pk}) # # def form_valid(self, form): # response = super().form_valid(form) # messages.success(self.request, 'Stock updated successfully.') # return response # # # # ============================================================================ # # PURCHASE ORDER VIEWS (RESTRICTED CRUD - Operational Data) # # ============================================================================ # # class PurchaseOrderListView(LoginRequiredMixin, ListView): # """ # List all purchase orders with filtering. # """ # model = PurchaseOrder # template_name = 'inventory/purchase_order_list.html' # context_object_name = 'orders' # paginate_by = 20 # # def get_queryset(self): # queryset = PurchaseOrder.objects.filter( # tenant=self.request.user.tenant # ).select_related('supplier') # # # Search functionality # search = self.request.GET.get('search') # if search: # queryset = queryset.filter( # Q(order_number__icontains=search) | # Q(supplier__supplier_name__icontains=search) # ) # # # Filter by status # status = self.request.GET.get('status') # if status: # queryset = queryset.filter(status=status) # # # Filter by supplier # supplier = self.request.GET.get('supplier') # if supplier: # queryset = queryset.filter(supplier_id=supplier) # # return queryset.order_by('-order_date') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context['statuses'] = PurchaseOrder.STATUS_CHOICES # context['suppliers'] = Supplier.objects.filter( # tenant=self.request.user.tenant, # is_active=True # ).order_by('supplier_name') # return context # # # class PurchaseOrderDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about a purchase order. # """ # model = PurchaseOrder # template_name = 'inventory/purchase_order_detail.html' # context_object_name = 'order' # # def get_queryset(self): # return PurchaseOrder.objects.filter( # tenant=self.request.user.tenant # ).select_related('supplier') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # order = self.get_object() # # # Order items # context['order_items'] = PurchaseOrderItem.objects.filter( # purchase_order=order # ).select_related('item').order_by('item__item_name') # # return context # # # class PurchaseOrderCreateView(LoginRequiredMixin, CreateView): # """ # Create a new purchase order. # """ # model = PurchaseOrder # form_class = PurchaseOrderForm # template_name = 'inventory/purchase_order_form.html' # success_url = reverse_lazy('inventory:purchase_order_list') # # def get_form_kwargs(self): # kwargs = super().get_form_kwargs() # kwargs['user'] = self.request.user # return kwargs # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # form.instance.created_by = self.request.user # response = super().form_valid(form) # # # Log the creation # AuditLogEntry.objects.create( # tenant=self.request.user.tenant, # user=self.request.user, # action='CREATE', # model_name='PurchaseOrder', # object_id=self.object.pk, # changes={'created': 'New purchase order created'} # ) # # messages.success(self.request, f'Purchase Order "{self.object.order_number}" created successfully.') # return response # # # class PurchaseOrderUpdateView(LoginRequiredMixin, UpdateView): # """ # Update a purchase order (limited updates based on status). # """ # model = PurchaseOrder # form_class = PurchaseOrderForm # template_name = 'inventory/purchase_order_form.html' # # def get_queryset(self): # return PurchaseOrder.objects.filter(tenant=self.request.user.tenant) # # def get_form_kwargs(self): # kwargs = super().get_form_kwargs() # kwargs['user'] = self.request.user # return kwargs # # def get_success_url(self): # return reverse('inventory:purchase_order_detail', kwargs={'pk': self.object.pk}) # # def form_valid(self, form): # # Check if order can be updated # if self.object.status in ['RECEIVED', 'CANCELLED']: # messages.error(self.request, 'Cannot update completed or cancelled orders.') # return redirect('inventory:purchase_order_detail', pk=self.object.pk) # # response = super().form_valid(form) # messages.success(self.request, f'Purchase Order "{self.object.order_number}" updated successfully.') # return response # # # # ============================================================================ # # HTMX AND AJAX VIEWS # # ============================================================================ # # @login_required # def inventory_stats(request): # """ # HTMX endpoint for real-time inventory statistics. # """ # user = request.user # # # Calculate statistics # stats = { # 'total_items': InventoryItem.objects.filter(tenant=user.tenant).count(), # 'low_stock_items': InventoryStock.objects.filter( # tenant=user.tenant, # quantity__lte=F('minimum_stock_level') # ).count(), # 'expired_items': InventoryStock.objects.filter( # tenant=user.tenant, # expiry_date__lte=timezone.now().date() # ).count(), # 'active_orders': PurchaseOrder.objects.filter( # tenant=user.tenant, # status__in=['PENDING', 'APPROVED', 'ORDERED'] # ).count(), # } # # return render(request, 'inventory/partials/inventory_stats.html', stats) # # # @login_required # def stock_search(request): # """ # HTMX endpoint for dynamic stock search. # """ # search = request.GET.get('search', '') # stock_items = InventoryStock.objects.filter( # tenant=request.user.tenant, # item__item_name__icontains=search # ).select_related('item', 'location')[:10] # # return render(request, 'inventory/partials/stock_search_results.html', { # 'stock_items': stock_items # }) # # # # ============================================================================ # # ACTION VIEWS FOR WORKFLOW OPERATIONS # # ============================================================================ # # @login_required # def adjust_stock(request, stock_id): # """ # Adjust inventory stock quantity. # """ # stock = get_object_or_404( # InventoryStock, # pk=stock_id, # tenant=request.user.tenant # ) # # if request.method == 'POST': # adjustment = request.POST.get('adjustment', 0) # reason = request.POST.get('reason', '') # # try: # adjustment = int(adjustment) # old_quantity = stock.quantity # stock.quantity = max(0, stock.quantity + adjustment) # stock.save() # # # Log the adjustment # AuditLogEntry.objects.create( # tenant=request.user.tenant, # user=request.user, # action='UPDATE', # model_name='InventoryStock', # object_id=stock.pk, # changes={ # 'adjustment': adjustment, # 'old_quantity': old_quantity, # 'new_quantity': stock.quantity, # 'reason': reason # } # ) # # messages.success(request, f'Stock adjusted successfully. New quantity: {stock.quantity}') # except ValueError: # messages.error(request, 'Invalid adjustment value.') # # return redirect('inventory:stock_detail', pk=stock.pk) # # # @login_required # def approve_purchase_order(request, order_id): # """ # Approve a purchase order. # """ # order = get_object_or_404( # PurchaseOrder, # pk=order_id, # tenant=request.user.tenant # ) # # if order.status == 'PENDING': # order.status = 'APPROVED' # order.approved_by = request.user # order.approved_date = timezone.now() # order.save() # # # Log the approval # AuditLogEntry.objects.create( # tenant=request.user.tenant, # user=request.user, # action='UPDATE', # model_name='PurchaseOrder', # object_id=order.pk, # changes={'status': 'APPROVED', 'approved_by': request.user.username} # ) # # messages.success(request, f'Purchase Order "{order.order_number}" approved successfully.') # else: # messages.error(request, 'Order cannot be approved in its current status.') # # return redirect('inventory:purchase_order_detail', pk=order.pk) # # # @login_required # def receive_purchase_order(request, order_id): # """ # Mark a purchase order as received and update stock. # """ # order = get_object_or_404( # PurchaseOrder, # pk=order_id, # tenant=request.user.tenant # ) # # if order.status == 'ORDERED': # order.status = 'RECEIVED' # order.received_date = timezone.now() # order.save() # # # Update stock for each item # for item in order.purchaseorderitem_set.all(): # stock, created = InventoryStock.objects.get_or_create( # tenant=request.user.tenant, # item=item.item, # location=order.delivery_location, # defaults={ # 'quantity': 0, # 'unit_cost': item.unit_price, # 'minimum_stock_level': 10 # } # ) # stock.quantity += item.quantity # stock.unit_cost = item.unit_price # Update with latest cost # stock.save() # # # Log the receipt # AuditLogEntry.objects.create( # tenant=request.user.tenant, # user=request.user, # action='UPDATE', # model_name='PurchaseOrder', # object_id=order.pk, # changes={'status': 'RECEIVED', 'received_date': timezone.now().isoformat()} # ) # # messages.success(request, f'Purchase Order "{order.order_number}" received and stock updated.') # else: # messages.error(request, 'Order cannot be received in its current status.') # # return redirect('inventory:purchase_order_detail', pk=order.pk) # # # #