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)