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 from django.db.models import Sum, Q, Count, F from datetime import timedelta from django.utils import timezone from django import forms from django.forms import inlineformset_factory from decimal import Decimal from .models import Supplier, PurchaseOrder, PurchaseOrderItem, GoodsReceipt, GoodsReceiptItem from apps.inventory.models import Product, Warehouse, Inventory, StockMovement class SupplierForm(forms.ModelForm): class Meta: model = Supplier fields = ['code', 'name', 'contact_person', 'email', 'phone', 'address', 'tax_id', 'payment_terms', 'is_active'] widgets = { 'address': forms.Textarea(attrs={'rows': 3}), 'payment_terms': forms.Textarea(attrs={'rows': 2}), } class PurchaseOrderItemForm(forms.ModelForm): class Meta: model = PurchaseOrderItem fields = ['product', 'quantity', 'unit_price'] widgets = { 'quantity': forms.NumberInput(attrs={'step': '0.01', 'min': '0'}), 'unit_price': forms.NumberInput(attrs={'step': '0.01', 'min': '0'}), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.fields['product'].queryset = Product.objects.filter(is_active=True) class PurchaseOrderForm(forms.ModelForm): class Meta: model = PurchaseOrder fields = ['supplier', 'order_date', 'expected_delivery_date', 'notes'] widgets = { 'order_date': forms.DateInput(attrs={'type': 'date'}), 'expected_delivery_date': forms.DateInput(attrs={'type': 'date'}), 'notes': forms.Textarea(attrs={'rows': 3}), } PurchaseOrderItemFormSet = inlineformset_factory( PurchaseOrder, PurchaseOrderItem, form=PurchaseOrderItemForm, extra=0, can_delete=True, min_num=1, validate_min=True ) @login_required def purchasing_dashboard(request): """Purchasing dashboard view""" suppliers = Supplier.objects.filter(is_active=True) pending_orders = PurchaseOrder.objects.filter( Q(status='ordered') | Q(status='processing') ) recent_orders = PurchaseOrder.objects.select_related('supplier').order_by('-created_at')[:5] # Calculate total value of recent orders (last 30 days) thirty_days_ago = timezone.now() - timedelta(days=30) total_value = PurchaseOrder.objects.filter( created_at__gte=thirty_days_ago ).aggregate(total=Sum('total_amount'))['total'] or 0 # Get top suppliers by order count top_suppliers = Supplier.objects.annotate( order_count=Count('purchaseorder') ).order_by('-order_count')[:5] context = { 'module_title': 'Purchasing Management', 'suppliers': suppliers, 'pending_orders': pending_orders, 'recent_orders': recent_orders, 'total_value': total_value, 'top_suppliers': top_suppliers, } return render(request, 'purchasing/dashboard.html', context) # Supplier Views @login_required @permission_required('purchasing.view_supplier', raise_exception=True) def supplier_list_view(request): """List all suppliers""" suppliers = Supplier.objects.all() context = { 'module_title': 'Supplier List', 'suppliers': suppliers, } return render(request, 'purchasing/supplier_list.html', context) @login_required def create_supplier_view(request): """Create a new supplier""" if request.method == 'POST': form = SupplierForm(request.POST) if form.is_valid(): supplier = form.save() messages.success(request, f'Supplier "{supplier.name}" created successfully!') return redirect('purchasing:supplier_detail', supplier_id=supplier.id) else: form = SupplierForm() context = { 'form': form, 'module_title': 'Create Supplier', 'is_create': True, } return render(request, 'purchasing/supplier_form.html', context) @login_required @permission_required('purchasing.view_supplier', raise_exception=True) def supplier_detail_view(request, supplier_id): """View supplier details""" try: supplier = Supplier.objects.get(id=supplier_id) context = { 'module_title': 'Supplier Details', 'supplier': supplier, } return render(request, 'purchasing/supplier_detail.html', context) except Supplier.DoesNotExist: messages.error(request, 'Supplier not found') return redirect('purchasing:supplier_list') @login_required def edit_supplier_view(request, supplier_id): """Edit supplier details""" supplier = get_object_or_404(Supplier, id=supplier_id) if request.method == 'POST': form = SupplierForm(request.POST, instance=supplier) if form.is_valid(): supplier = form.save() messages.success(request, f'Supplier "{supplier.name}" updated successfully!') return redirect('purchasing:supplier_detail', supplier_id=supplier.id) else: form = SupplierForm(instance=supplier) context = { 'form': form, 'supplier': supplier, 'module_title': 'Edit Supplier', 'is_create': False, } return render(request, 'purchasing/supplier_form.html', context) @login_required def delete_supplier_view(request, supplier_id): """Delete a supplier""" supplier = get_object_or_404(Supplier, id=supplier_id) if request.method == 'POST': supplier_name = supplier.name supplier.delete() messages.success(request, f'Supplier "{supplier_name}" deleted successfully!') return redirect('purchasing:supplier_list') # Get supplier statistics for confirmation supplier_order_count = PurchaseOrder.objects.filter(supplier=supplier).count() supplier_total_value = PurchaseOrder.objects.filter(supplier=supplier).aggregate( total=Sum('total_amount') )['total'] or 0 context = { 'supplier': supplier, 'module_title': 'Delete Supplier', 'supplier_order_count': supplier_order_count, 'supplier_total_value': supplier_total_value, } return render(request, 'purchasing/supplier_confirm_delete.html', context) # Purchase Order Views @login_required @permission_required('purchasing.view_purchaseorder', raise_exception=True) def po_list_view(request): """List all purchase orders""" pos = PurchaseOrder.objects.select_related('supplier').all() # Calculate statistics total_value = pos.aggregate(total=Sum('total_amount'))['total'] or 0 pending_orders = pos.filter(status__in=['ordered', 'processing']).count() context = { 'module_title': 'Purchase Order List', 'pos': pos, 'total_value': total_value, 'pending_orders': pending_orders, } return render(request, 'purchasing/po_list.html', context) @login_required def create_po_view(request): """Create a new purchase order""" if request.method == 'POST': form = PurchaseOrderForm(request.POST) formset = PurchaseOrderItemFormSet(request.POST) if form.is_valid() and formset.is_valid(): # Save the purchase order po = form.save(commit=False) po.created_by = request.user po.save() # Save the items items = formset.save(commit=False) for item in items: item.po = po item.save() # Calculate totals po.subtotal = sum(item.total_price for item in po.items.all()) po.tax_amount = po.subtotal * Decimal('0.11') # 11% tax po.total_amount = po.subtotal + po.tax_amount po.save() messages.success(request, f'Purchase Order "{po.po_number}" created successfully!') return redirect('purchasing:po_detail', po_number=po.po_number) else: form = PurchaseOrderForm() formset = PurchaseOrderItemFormSet() context = { 'form': form, 'formset': formset, 'module_title': 'Create Purchase Order', 'is_create': True, } return render(request, 'purchasing/po_form.html', context) @login_required @permission_required('purchasing.view_purchaseorder', raise_exception=True) def po_detail_view(request, po_number): """View purchase order details""" try: po = PurchaseOrder.objects.get(po_number=po_number) context = { 'module_title': 'Purchase Order Details', 'po': po, } return render(request, 'purchasing/po_detail.html', context) except PurchaseOrder.DoesNotExist: messages.error(request, 'Purchase order not found') return redirect('purchasing:po_list') @login_required def edit_po_view(request, po_number): """Edit purchase order details""" po = get_object_or_404(PurchaseOrder, po_number=po_number) if request.method == 'POST': form = PurchaseOrderForm(request.POST, instance=po) formset = PurchaseOrderItemFormSet(request.POST, instance=po) # Custom validation to skip deleted items formset_valid = True for i, form_item in enumerate(formset): # Check if this item is marked for deletion in POST data delete_field_name = f'form-{i}-DELETE' is_deleted = request.POST.get(delete_field_name, 'off') == 'on' if not is_deleted: # Only validate non-deleted items if not form_item.is_valid(): formset_valid = False if form.is_valid() and formset_valid: # Save the purchase order po = form.save(commit=False) po.save() # Save the items (excluding deleted ones) items = formset.save(commit=False) for item in items: item.po = po item.save() # Delete items marked for deletion for deleted_item in formset.deleted_objects: deleted_item.delete() # Recalculate totals po.subtotal = sum(item.total_price for item in po.items.all()) po.tax_amount = po.subtotal * Decimal('0.11') # 11% tax po.total_amount = po.subtotal + po.tax_amount po.save() messages.success(request, f'Purchase Order "{po.po_number}" updated successfully!') return redirect('purchasing:po_detail', po_number=po.po_number) else: form = PurchaseOrderForm(instance=po) formset = PurchaseOrderItemFormSet(instance=po) context = { 'form': form, 'formset': formset, 'po': po, 'module_title': 'Edit Purchase Order', 'is_create': False, } return render(request, 'purchasing/po_form.html', context) @login_required def approve_po_view(request, po_number): """Approve a purchase order""" po = get_object_or_404(PurchaseOrder, po_number=po_number) if request.method == 'POST': if po.status == 'ordered': po.status = 'approved' po.save() messages.success(request, f'Purchase Order "{po.po_number}" has been approved successfully!') # Log the approval # You could add audit logging here else: messages.warning(request, f'Purchase Order "{po.po_number}" cannot be approved in its current status.') return redirect('purchasing:po_detail', po_number=po.po_number) context = { 'po': po, 'action': 'approve', 'action_title': 'Approve Purchase Order', 'action_message': f'Are you sure you want to approve Purchase Order "{po.po_number}"?', 'action_button': 'Approve Order', 'action_class': 'success', } return render(request, 'purchasing/po_action_confirm.html', context) @login_required def cancel_po_view(request, po_number): """Cancel a purchase order""" po = get_object_or_404(PurchaseOrder, po_number=po_number) if request.method == 'POST': if po.status in ['ordered', 'approved']: po.status = 'cancelled' po.save() messages.success(request, f'Purchase Order "{po.po_number}" has been cancelled successfully!') # Log the cancellation # You could add audit logging here else: messages.warning(request, f'Purchase Order "{po.po_number}" cannot be cancelled in its current status.') return redirect('purchasing:po_detail', po_number=po.po_number) context = { 'po': po, 'action': 'cancel', 'action_title': 'Cancel Purchase Order', 'action_message': f'Are you sure you want to cancel Purchase Order "{po.po_number}"? This action cannot be undone.', 'action_button': 'Cancel Order', 'action_class': 'danger', } return render(request, 'purchasing/po_action_confirm.html', context) @login_required def delete_po_view(request, po_number): """Delete a purchase order""" po = get_object_or_404(PurchaseOrder, po_number=po_number) if request.method == 'POST': po_number = po.po_number po.delete() messages.success(request, f'Purchase Order "{po_number}" deleted successfully!') return redirect('purchasing:po_list') context = { 'po': po, 'module_title': 'Delete Purchase Order', } return render(request, 'purchasing/po_confirm_delete.html', context) # Goods Receipt Views @login_required @permission_required('purchasing.view_goodsreceipt', raise_exception=True) def gr_list_view(request): """List all goods receipts""" grs = GoodsReceipt.objects.all() context = { 'module_title': 'Goods Receipt List', 'grs': grs, } return render(request, 'purchasing/gr_list.html', context) @login_required def create_gr_view(request, po_number): """Create a new goods receipt""" po = get_object_or_404(PurchaseOrder, po_number=po_number) # Check if PO can have goods receipt if po.status not in ['ordered', 'approved']: messages.error(request, f'Cannot create goods receipt for Purchase Order in "{po.get_status_display()}" status.') return redirect('purchasing:po_detail', po_number=po.po_number) if request.method == 'POST': # Generate GR number (simple auto-increment) last_gr = GoodsReceipt.objects.order_by('-id').first() gr_number = f"GR{str(last_gr.id + 1).zfill(4)}" if last_gr else "GR0001" # Create goods receipt gr = GoodsReceipt.objects.create( gr_number=gr_number, po=po, receipt_date=request.POST.get('receipt_date', timezone.now().date()), received_by=request.user, notes=request.POST.get('notes', '') ) # Create goods receipt items and update inventory for po_item in po.items.all(): # Convert Indonesian format (comma) to standard format (dot) for Decimal quantity_str = request.POST.get(f'quantity_{po_item.id}', '0').replace(',', '.') received_qty = Decimal(quantity_str) if quantity_str else Decimal('0') if received_qty > 0: # Create GR item GoodsReceiptItem.objects.create( receipt=gr, po_item=po_item, received_quantity=received_qty, notes=request.POST.get(f'notes_{po_item.id}', '') ) # Update PO item received quantity po_item.received_quantity += received_qty po_item.save() # Update inventory warehouse = Warehouse.objects.first() # Default to first warehouse if not specified inventory, created = Inventory.objects.get_or_create( product=po_item.product, warehouse=warehouse, defaults={'quantity': 0} ) inventory.quantity += received_qty inventory.save() # Create stock movement record StockMovement.objects.create( product=po_item.product, warehouse=warehouse, movement_type='in', quantity=received_qty, reference_number=f"GR{gr.gr_number}", notes=f"Goods receipt from PO {po.po_number}", created_by=request.user ) # Check if PO is fully received total_received = sum(item.received_quantity for item in po.items.all()) total_ordered = sum(item.quantity for item in po.items.all()) if total_received >= total_ordered: po.status = 'completed' po.save() messages.success(request, f'Goods Receipt "{gr.gr_number}" created successfully! Purchase Order is now completed.') else: messages.success(request, f'Goods Receipt "{gr.gr_number}" created successfully! Partial receipt recorded.') return redirect('purchasing:gr_detail', gr_number=gr.gr_number) context = { 'po': po, 'module_title': f'Create Goods Receipt for {po.po_number}', 'today': timezone.now().date(), } return render(request, 'purchasing/gr_form.html', context) @login_required @permission_required('purchasing.view_goodsreceipt', raise_exception=True) def gr_detail_view(request, gr_number): """View goods receipt details""" try: gr = GoodsReceipt.objects.get(gr_number=gr_number) # Calculate totals total_quantity = sum(item.received_quantity for item in gr.items.all()) total_value = sum(item.po_item.unit_price * item.received_quantity for item in gr.items.all()) # Calculate previously received quantities and item totals for each item for item in gr.items.all(): # Previously received = current total received - this receipt quantity item.previously_received = item.po_item.received_quantity - item.received_quantity # Calculate item total value item.item_total = item.po_item.unit_price * item.received_quantity context = { 'module_title': 'Goods Receipt Details', 'gr': gr, 'total_quantity': total_quantity, 'total_value': total_value, } return render(request, 'purchasing/gr_detail.html', context) except GoodsReceipt.DoesNotExist: messages.error(request, 'Goods receipt not found') return redirect('purchasing:gr_list')