2083 lines
71 KiB
Python
2083 lines
71 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
|
|
)
|
|
|
|
|
|
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):
|
|
tenant = self.request.user.tenant
|
|
queryset = InventoryItem.objects.filter(tenant=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.ItemCategory.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):
|
|
tenant = self.request.user.tenant
|
|
return InventoryItem.objects.filter(tenant=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):
|
|
tenant = self.request.user.tenant
|
|
form.instance.tenant = tenant
|
|
response = super().form_valid(form)
|
|
|
|
# Log the creation
|
|
AuditLogEntry.objects.create(
|
|
tenant=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):
|
|
tenant = self.request.user.tenant
|
|
return InventoryItem.objects.filter(tenant=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):
|
|
tenant = self.request.user.tenant
|
|
return InventoryItem.objects.filter(tenant=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):
|
|
tenant = self.request.user.tenant
|
|
queryset = InventoryStock.objects.filter(
|
|
inventory_item__tenant=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)
|
|
#
|
|
#
|
|
#
|
|
#
|