Django_Basic_Manufacturing_3/apps/inventory/views.py
2025-08-22 17:05:22 +07:00

695 lines
25 KiB
Python

from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required, permission_required
from django.contrib import messages
from django.http import HttpResponse, JsonResponse
from django.db.models import Sum, Q, Count
from django import forms
from django.utils import timezone
from .models import Product, Category, UnitOfMeasure, Warehouse, StockMovement, Inventory
import locale
from decimal import Decimal, InvalidOperation
class IndonesianNumberInput(forms.NumberInput):
"""Custom widget for Indonesian number input formatting"""
def __init__(self, attrs=None):
if attrs is None:
attrs = {}
# Set step to 0.01 for decimal fields
if 'step' not in attrs:
attrs['step'] = '0.01'
# Add placeholder for better UX
if 'placeholder' not in attrs:
attrs['placeholder'] = '0.00'
super().__init__(attrs)
def format_value(self, value):
"""Format the value for display in the input field"""
if value is None or value == '':
return None
try:
# Convert to Decimal for precise handling
if not isinstance(value, Decimal):
value = Decimal(str(value))
# Format with Indonesian decimal separator (comma)
formatted = '{:.2f}'.format(value)
return formatted.replace('.', ',')
except (ValueError, TypeError):
return super().format_value(value)
def value_from_datadict(self, data, files, name):
"""Handle value from form data"""
value = super().value_from_datadict(data, files, name)
if value is None or value == '':
return value
try:
# Clean up any formatting and convert to Decimal
if isinstance(value, str):
# Remove any thousand separators and convert comma to dot
value = value.replace('.', '').replace(',', '.')
return Decimal(value)
except (ValueError, TypeError, InvalidOperation):
return value
class ProductForm(forms.ModelForm):
class Meta:
model = Product
fields = ['code', 'name', 'description', 'category', 'unit_of_measure',
'product_type', 'reorder_level', 'selling_price', 'cost_price', 'is_active']
widgets = {
'description': forms.Textarea(attrs={'rows': 3}),
'reorder_level': IndonesianNumberInput(attrs={'min': 0}),
'selling_price': IndonesianNumberInput(attrs={'min': 0, 'step': '0.01'}),
'cost_price': IndonesianNumberInput(attrs={'min': 0, 'step': '0.01'}),
}
class CategoryForm(forms.ModelForm):
class Meta:
model = Category
fields = ['name', 'description']
widgets = {
'description': forms.Textarea(attrs={'rows': 3}),
}
class UnitOfMeasureForm(forms.ModelForm):
class Meta:
model = UnitOfMeasure
fields = ['name', 'abbreviation']
class WarehouseForm(forms.ModelForm):
class Meta:
model = Warehouse
fields = ['name', 'location', 'is_active']
@login_required
def inventory_dashboard(request):
"""Inventory dashboard view"""
products = Product.objects.filter(is_active=True)
warehouses = Warehouse.objects.filter(is_active=True)
categories = Category.objects.all()
# Get low stock products (below reorder level)
low_stock_products = []
for product in products:
total_quantity = Inventory.objects.filter(
product=product
).aggregate(total=Sum('quantity'))['total'] or 0
if total_quantity <= product.reorder_level:
low_stock_products.append({
'name': product.name,
'total_quantity': total_quantity,
'reorder_level': product.reorder_level,
})
# Get recent stock movements
recent_movements = StockMovement.objects.select_related(
'product', 'warehouse'
).order_by('-created_at')[:10]
context = {
'module_title': 'Inventory Management',
'products': products,
'warehouses': warehouses,
'categories': categories,
'low_stock_products': low_stock_products,
'recent_movements': recent_movements,
}
return render(request, 'inventory/dashboard.html', context)
# Product Views
@login_required
@permission_required('inventory.view_product', raise_exception=True)
def product_list_view(request):
"""List all products"""
products = Product.objects.all()
# Add current stock information to each product
for product in products:
total_stock = Inventory.objects.filter(product=product).aggregate(
total=Sum('quantity')
)['total'] or 0
product.current_stock = total_stock
context = {
'module_title': 'Product List',
'products': products,
}
return render(request, 'inventory/product_list.html', context)
@login_required
@permission_required('inventory.add_product', raise_exception=True)
def create_product_view(request):
"""Create a new product"""
if request.method == 'POST':
form = ProductForm(request.POST)
if form.is_valid():
product = form.save()
messages.success(request, f'Product "{product.name}" created successfully!')
return redirect('inventory:product_detail', product_id=product.id)
else:
form = ProductForm()
context = {
'form': form,
'module_title': 'Create Product',
'is_create': True,
}
return render(request, 'inventory/product_form.html', context)
@login_required
@permission_required('inventory.view_product', raise_exception=True)
def product_detail_view(request, product_id):
"""View product details"""
try:
product = Product.objects.get(id=product_id)
inventory_items = Inventory.objects.filter(product=product).select_related('warehouse')
total_stock = inventory_items.aggregate(total=Sum('quantity'))['total'] or 0
recent_movements = StockMovement.objects.filter(
product=product
).select_related('warehouse').order_by('-created_at')[:10]
context = {
'module_title': 'Product Details',
'product': product,
'inventory_items': inventory_items,
'total_stock': total_stock,
'recent_movements': recent_movements,
}
return render(request, 'inventory/product_detail.html', context)
except Product.DoesNotExist:
messages.error(request, 'Product not found')
return redirect('inventory:product_list')
@login_required
@permission_required('inventory.change_product', raise_exception=True)
def edit_product_view(request, product_id):
"""Edit product details"""
product = get_object_or_404(Product, id=product_id)
if request.method == 'POST':
form = ProductForm(request.POST, instance=product)
if form.is_valid():
product = form.save()
messages.success(request, f'Product "{product.name}" updated successfully!')
return redirect('inventory:product_detail', product_id=product.id)
else:
form = ProductForm(instance=product)
context = {
'form': form,
'product': product,
'module_title': 'Edit Product',
'is_create': False,
}
return render(request, 'inventory/product_form.html', context)
@login_required
def delete_product_view(request, product_id):
"""Delete a product"""
product = get_object_or_404(Product, id=product_id)
if request.method == 'POST':
# Simple delete without permission checks for now
try:
product_name = product.name
product.delete()
messages.success(request, f'Product "{product_name}" deleted successfully!')
return redirect('inventory:product_list')
except Exception as e:
messages.error(request, f'Error deleting product: {str(e)}')
return redirect('inventory:product_detail', product_id=product.id)
# Get product statistics for confirmation
inventory_count = Inventory.objects.filter(product=product).count()
movement_count = StockMovement.objects.filter(product=product).count()
total_stock = Inventory.objects.filter(product=product).aggregate(
total=Sum('quantity')
)['total'] or 0
context = {
'product': product,
'module_title': 'Delete Product',
'inventory_count': inventory_count,
'movement_count': movement_count,
'total_stock': total_stock,
}
return render(request, 'inventory/product_confirm_delete.html', context)
# Category Views
@login_required
@permission_required('inventory.view_category', raise_exception=True)
def category_list_view(request):
"""List all categories"""
categories = Category.objects.all()
context = {
'module_title': 'Category List',
'categories': categories,
}
return render(request, 'inventory/category_list.html', context)
@login_required
@permission_required('inventory.add_category', raise_exception=True)
def create_category_view(request):
"""Create a new category"""
if request.method == 'POST':
form = CategoryForm(request.POST)
if form.is_valid():
category = form.save()
messages.success(request, f'Category "{category.name}" created successfully!')
return redirect('inventory:category_list')
else:
form = CategoryForm()
context = {
'form': form,
'module_title': 'Create Category',
}
return render(request, 'inventory/category_form.html', context)
@login_required
@permission_required('inventory.change_category', raise_exception=True)
def edit_category_view(request, category_id):
"""Edit category details"""
# In a real app, this would contain category editing logic
return HttpResponse("Edit category page")
@login_required
@permission_required('inventory.delete_category', raise_exception=True)
def delete_category_view(request, category_id):
"""Delete a category"""
# In a real app, this would contain category deletion logic
return HttpResponse("Delete category page")
# Unit of Measure Views
@login_required
@permission_required('inventory.view_unitofmeasure', raise_exception=True)
def uom_list_view(request):
"""List all units of measure"""
uoms = UnitOfMeasure.objects.all()
context = {
'module_title': 'Unit of Measure List',
'uoms': uoms,
}
return render(request, 'inventory/uom_list.html', context)
@login_required
@permission_required('inventory.add_unitofmeasure', raise_exception=True)
def create_uom_view(request):
"""Create a new unit of measure"""
# In a real app, this would contain UOM creation logic
return HttpResponse("Create unit of measure page")
@login_required
@permission_required('inventory.change_unitofmeasure', raise_exception=True)
def edit_uom_view(request, uom_id):
"""Edit unit of measure details"""
# In a real app, this would contain UOM editing logic
return HttpResponse("Edit unit of measure page")
@login_required
@permission_required('inventory.delete_unitofmeasure', raise_exception=True)
def delete_uom_view(request, uom_id):
"""Delete a unit of measure"""
# In a real app, this would contain UOM deletion logic
return HttpResponse("Delete unit of measure page")
# Warehouse Views
@login_required
@permission_required('inventory.view_warehouse', raise_exception=True)
def warehouse_list_view(request):
"""List all warehouses"""
warehouses = Warehouse.objects.prefetch_related('inventory_set').all()
# Add inventory count to each warehouse
for warehouse in warehouses:
warehouse.inventory_count = warehouse.inventory_set.count()
context = {
'module_title': 'Warehouse List',
'warehouses': warehouses,
}
return render(request, 'inventory/warehouse_list.html', context)
@login_required
@permission_required('inventory.view_warehouse', raise_exception=True)
def warehouse_detail_view(request, warehouse_id):
"""View warehouse details"""
try:
warehouse = Warehouse.objects.get(id=warehouse_id)
inventory_items = Inventory.objects.filter(warehouse=warehouse).select_related('product')
total_products = inventory_items.count()
total_value = sum(item.quantity * item.product.cost_price for item in inventory_items)
# Add total_value to each inventory item for template use
for item in inventory_items:
item.total_value = item.quantity * item.product.cost_price
context = {
'module_title': 'Warehouse Details',
'warehouse': warehouse,
'inventory_items': inventory_items,
'total_products': total_products,
'total_value': total_value,
}
return render(request, 'inventory/warehouse_detail.html', context)
except Warehouse.DoesNotExist:
messages.error(request, 'Warehouse not found')
return redirect('inventory:warehouse_list')
@login_required
def create_warehouse_view(request):
"""Create a new warehouse"""
if request.method == 'POST':
form = WarehouseForm(request.POST)
if form.is_valid():
warehouse = form.save()
messages.success(request, f'Warehouse "{warehouse.name}" created successfully!')
return redirect('inventory:warehouse_list')
else:
form = WarehouseForm()
context = {
'form': form,
'module_title': 'Create Warehouse',
'is_create': True,
}
return render(request, 'inventory/warehouse_form.html', context)
@login_required
def edit_warehouse_view(request, warehouse_id):
"""Edit warehouse details"""
warehouse = get_object_or_404(Warehouse, id=warehouse_id)
if request.method == 'POST':
form = WarehouseForm(request.POST, instance=warehouse)
if form.is_valid():
warehouse = form.save()
messages.success(request, f'Warehouse "{warehouse.name}" updated successfully!')
return redirect('inventory:warehouse_list')
else:
form = WarehouseForm(instance=warehouse)
context = {
'form': form,
'warehouse': warehouse,
'module_title': 'Edit Warehouse',
'is_create': False,
}
return render(request, 'inventory/warehouse_form.html', context)
@login_required
def delete_warehouse_view(request, warehouse_id):
"""Delete a warehouse"""
warehouse = get_object_or_404(Warehouse, id=warehouse_id)
if request.method == 'POST':
warehouse_name = warehouse.name
warehouse.delete()
messages.success(request, f'Warehouse "{warehouse_name}" deleted successfully!')
return redirect('inventory:warehouse_list')
# Get warehouse statistics for confirmation
inventory_items = Inventory.objects.filter(warehouse=warehouse).select_related('product')
inventory_count = inventory_items.count()
total_products = inventory_items.values('product').distinct().count()
total_value = sum(item.quantity * item.product.cost_price for item in inventory_items)
context = {
'warehouse': warehouse,
'module_title': 'Delete Warehouse',
'inventory_count': inventory_count,
'total_products': total_products,
'total_value': total_value,
}
return render(request, 'inventory/warehouse_confirm_delete.html', context)
# Stock Movement Views
@login_required
@permission_required('inventory.view_stockmovement', raise_exception=True)
def stock_movement_list_view(request):
"""List all stock movements"""
movements = StockMovement.objects.all()
context = {
'module_title': 'Stock Movement List',
'movements': movements,
}
return render(request, 'inventory/movement_list.html', context)
@login_required
@permission_required('inventory.add_stockmovement', raise_exception=True)
def stock_in_view(request):
"""Add stock to inventory"""
if request.method == 'POST':
product_id = request.POST.get('product')
warehouse_id = request.POST.get('warehouse')
quantity_str = request.POST.get('quantity', '0').replace(',', '.')
try:
product = Product.objects.get(id=product_id)
warehouse = Warehouse.objects.get(id=warehouse_id)
quantity = Decimal(quantity_str)
if quantity <= 0:
messages.error(request, 'Quantity must be greater than 0')
return redirect('inventory:stock_in')
# Create or update inventory
inventory, created = Inventory.objects.get_or_create(
product=product,
warehouse=warehouse,
defaults={'quantity': 0}
)
inventory.quantity += quantity
inventory.save()
# Create stock movement record
StockMovement.objects.create(
product=product,
warehouse=warehouse,
movement_type='in',
quantity=quantity,
reference_number=f"STKIN-{timezone.now().strftime('%Y%m%d-%H%M%S')}",
notes=request.POST.get('notes', ''),
created_by=request.user
)
messages.success(request, f'Successfully added {quantity} {product.unit_of_measure.abbreviation} of {product.name} to {warehouse.name}')
return redirect('inventory:movement_list')
except (Product.DoesNotExist, Warehouse.DoesNotExist):
messages.error(request, 'Invalid product or warehouse selected')
except (ValueError, InvalidOperation):
messages.error(request, 'Invalid quantity entered')
# GET request - show form
context = {
'module_title': 'Stock In',
'products': Product.objects.filter(is_active=True),
'warehouses': Warehouse.objects.filter(is_active=True),
}
return render(request, 'inventory/stock_in.html', context)
@login_required
@permission_required('inventory.add_stockmovement', raise_exception=True)
def stock_out_view(request):
"""Remove stock from inventory"""
if request.method == 'POST':
product_id = request.POST.get('product')
warehouse_id = request.POST.get('warehouse')
quantity_str = request.POST.get('quantity', '0').replace(',', '.')
try:
product = Product.objects.get(id=product_id)
warehouse = Warehouse.objects.get(id=warehouse_id)
quantity = Decimal(quantity_str)
if quantity <= 0:
messages.error(request, 'Quantity must be greater than 0')
return redirect('inventory:stock_out')
# Check if sufficient stock is available
inventory = Inventory.objects.filter(
product=product, warehouse=warehouse
).first()
if not inventory or inventory.quantity < quantity:
messages.error(request, f'Insufficient stock. Available: {inventory.quantity if inventory else 0} {product.unit_of_measure.abbreviation}')
return redirect('inventory:stock_out')
# Update inventory
inventory.quantity -= quantity
inventory.save()
# Create stock movement record
StockMovement.objects.create(
product=product,
warehouse=warehouse,
movement_type='out',
quantity=quantity,
reference_number=f"STKOUT-{timezone.now().strftime('%Y%m%d-%H%M%S')}",
notes=request.POST.get('notes', ''),
created_by=request.user
)
messages.success(request, f'Successfully removed {quantity} {product.unit_of_measure.abbreviation} of {product.name} from {warehouse.name}')
return redirect('inventory:movement_list')
except (Product.DoesNotExist, Warehouse.DoesNotExist):
messages.error(request, 'Invalid product or warehouse selected')
except (ValueError, InvalidOperation):
messages.error(request, 'Invalid quantity entered')
# GET request - show form
context = {
'module_title': 'Stock Out',
'products': Product.objects.filter(is_active=True),
'warehouses': Warehouse.objects.filter(is_active=True),
}
return render(request, 'inventory/stock_out.html', context)
@login_required
@permission_required('inventory.add_stockmovement', raise_exception=True)
def stock_adjustment_view(request):
"""Adjust stock quantity"""
# In a real app, this would contain stock adjustment logic
return HttpResponse("Stock adjustment page")
@login_required
@permission_required('inventory.add_stockmovement', raise_exception=True)
def stock_transfer_view(request):
"""Transfer stock between warehouses"""
if request.method == 'POST':
product_id = request.POST.get('product')
from_warehouse_id = request.POST.get('from_warehouse')
to_warehouse_id = request.POST.get('to_warehouse')
quantity_str = request.POST.get('quantity', '0').replace(',', '.')
try:
product = Product.objects.get(id=product_id)
from_warehouse = Warehouse.objects.get(id=from_warehouse_id)
to_warehouse = Warehouse.objects.get(id=to_warehouse_id)
quantity = Decimal(quantity_str)
if quantity <= 0:
messages.error(request, 'Quantity must be greater than 0')
return redirect('inventory:stock_transfer')
if from_warehouse == to_warehouse:
messages.error(request, 'Source and destination warehouses must be different')
return redirect('inventory:stock_transfer')
# Check if sufficient stock is available in source warehouse
from_inventory = Inventory.objects.filter(
product=product, warehouse=from_warehouse
).first()
if not from_inventory or from_inventory.quantity < quantity:
messages.error(request, f'Insufficient stock in {from_warehouse.name}. Available: {from_inventory.quantity if from_inventory else 0} {product.unit_of_measure.abbreviation}')
return redirect('inventory:stock_transfer')
# Remove from source warehouse
from_inventory.quantity -= quantity
from_inventory.save()
# Add to destination warehouse
to_inventory, created = Inventory.objects.get_or_create(
product=product,
warehouse=to_warehouse,
defaults={'quantity': 0}
)
to_inventory.quantity += quantity
to_inventory.save()
# Create stock movement records
reference_number = f"TRANSFER-{timezone.now().strftime('%Y%m%d-%H%M%S')}"
# Out movement from source
StockMovement.objects.create(
product=product,
warehouse=from_warehouse,
movement_type='out',
quantity=quantity,
reference_number=reference_number,
notes=f"Transfer to {to_warehouse.name}: {request.POST.get('notes', '')}",
created_by=request.user
)
# In movement to destination
StockMovement.objects.create(
product=product,
warehouse=to_warehouse,
movement_type='in',
quantity=quantity,
reference_number=reference_number,
notes=f"Transfer from {from_warehouse.name}: {request.POST.get('notes', '')}",
created_by=request.user
)
messages.success(request, f'Successfully transferred {quantity} {product.unit_of_measure.abbreviation} of {product.name} from {from_warehouse.name} to {to_warehouse.name}')
return redirect('inventory:movement_list')
except (Product.DoesNotExist, Warehouse.DoesNotExist):
messages.error(request, 'Invalid product or warehouse selected')
except (ValueError, InvalidOperation):
messages.error(request, 'Invalid quantity entered')
# GET request - show form
context = {
'module_title': 'Stock Transfer',
'products': Product.objects.filter(is_active=True),
'warehouses': Warehouse.objects.filter(is_active=True),
}
return render(request, 'inventory/stock_transfer.html', context)
@login_required
def product_api_view(request):
"""API endpoint to get product data with unit of measure information"""
products = Product.objects.select_related('unit_of_measure').values(
'id', 'name', 'unit_of_measure__id', 'unit_of_measure__name'
)
product_list = []
for product in products:
product_list.append({
'id': product['id'],
'name': product['name'],
'unit_of_measure': {
'id': product['unit_of_measure__id'],
'name': product['unit_of_measure__name']
} if product['unit_of_measure__id'] else None
})
return JsonResponse(product_list, safe=False)