Marwan Alwali 1f0a6bff5f update
2025-08-31 12:21:16 +03:00

2105 lines
72 KiB
Python

"""
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('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__lte=F('minimum_stock_level')
# ).select_related('item', 'location').order_by('quantity')[:10]
# context['recent_stock_movements'] = InventoryStock.objects.filter(
# 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/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)
# ============================================================================
# INVENTORY LOCATION VIEWS (FULL CRUD - Master Data)
# ============================================================================
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
# ============================================================================
# INVENTORY ITEM VIEWS (FULL CRUD - Master Data)
# ============================================================================
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)
# ============================================================================
# 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/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(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/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
# ============================================================================
# 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(
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(),
}
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)
#
# """
# 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)
#
#
#
#