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 import forms from django.db.models import Sum, Q, F from decimal import Decimal from django.utils import timezone from .models import Customer, SalesOrder, Delivery, DeliveryItem, SalesOrderItem from apps.inventory.models import Product, Warehouse, Inventory, StockMovement class CustomerForm(forms.ModelForm): class Meta: model = Customer 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}), } @login_required def sales_dashboard(request): """Sales dashboard view""" from django.db.models import Count, Sum, Q from django.utils import timezone from datetime import timedelta # Get dashboard data customers = Customer.objects.filter(is_active=True) pending_orders = SalesOrder.objects.filter( Q(status='ordered') | Q(status='processing') ) recent_orders = SalesOrder.objects.select_related('customer').order_by('-order_date')[:10] # Calculate total value of recent orders (last 30 days) thirty_days_ago = timezone.now().date() - timedelta(days=30) total_value = SalesOrder.objects.filter( order_date__gte=thirty_days_ago ).aggregate( total=Sum('total_amount') )['total'] or 0 # Get top customers by order count top_customers = Customer.objects.annotate( order_count=Count('salesorder') ).order_by('-order_count')[:5] context = { 'module_title': 'Sales Management', 'customers': customers, 'pending_orders': pending_orders, 'total_value': total_value, 'recent_orders': recent_orders, 'top_customers': top_customers, } return render(request, 'sales/dashboard.html', context) # Customer Views @login_required @permission_required('sales.view_customer', raise_exception=True) def customer_list_view(request): """List all customers""" customers = Customer.objects.all() context = { 'module_title': 'Customer List', 'customers': customers, } return render(request, 'sales/customer_list.html', context) @login_required @permission_required('sales.add_customer', raise_exception=True) def create_customer_view(request): """Create a new customer""" if request.method == 'POST': form = CustomerForm(request.POST) if form.is_valid(): customer = form.save() messages.success(request, f'Customer "{customer.name}" created successfully!') return redirect('sales:customer_detail', customer_id=customer.id) else: form = CustomerForm() context = { 'form': form, 'module_title': 'Create Customer', 'is_create': True, } return render(request, 'sales/customer_form.html', context) @login_required @permission_required('sales.view_customer', raise_exception=True) def customer_detail_view(request, customer_id): """View customer details""" try: customer = Customer.objects.get(id=customer_id) # Get customer's sales orders sales_orders = SalesOrder.objects.filter(customer=customer).order_by('-order_date')[:10] # Calculate statistics total_orders = SalesOrder.objects.filter(customer=customer).count() total_value = SalesOrder.objects.filter(customer=customer).aggregate( total=Sum('total_amount') )['total'] or 0 pending_orders = SalesOrder.objects.filter( customer=customer, status__in=['processing'] ).count() # For delete confirmation customer_order_count = total_orders customer_total_value = total_value context = { 'module_title': 'Customer Details', 'customer': customer, 'sales_orders': sales_orders, 'total_orders': total_orders, 'total_value': total_value, 'pending_orders': pending_orders, 'customer_order_count': customer_order_count, 'customer_total_value': customer_total_value, } return render(request, 'sales/customer_detail.html', context) except Customer.DoesNotExist: messages.error(request, 'Customer not found') return redirect('sales:customer_list') @login_required @permission_required('sales.change_customer', raise_exception=True) def edit_customer_view(request, customer_id): """Edit customer details""" customer = get_object_or_404(Customer, id=customer_id) if request.method == 'POST': form = CustomerForm(request.POST, instance=customer) if form.is_valid(): customer = form.save() messages.success(request, f'Customer "{customer.name}" updated successfully!') return redirect('sales:customer_detail', customer_id=customer.id) else: form = CustomerForm(instance=customer) context = { 'form': form, 'customer': customer, 'module_title': 'Edit Customer', 'is_create': False, } return render(request, 'sales/customer_form.html', context) @login_required @permission_required('sales.delete_customer', raise_exception=True) def delete_customer_view(request, customer_id): """Delete a customer""" customer = get_object_or_404(Customer, id=customer_id) if request.method == 'POST': customer_name = customer.name customer.delete() messages.success(request, f'Customer "{customer_name}" deleted successfully!') return redirect('sales:customer_list') context = { 'customer': customer, 'module_title': 'Delete Customer', } return render(request, 'sales/customer_confirm_delete.html', context) # Sales Order Views @login_required @permission_required('sales.view_salesorder', raise_exception=True) def so_list_view(request): """List all sales orders""" sos = SalesOrder.objects.all().select_related('customer') # Calculate summary statistics total_value = SalesOrder.objects.aggregate( total=Sum('total_amount') )['total'] or 0 pending_orders = SalesOrder.objects.filter( status__in=['processing'] ).count() context = { 'module_title': 'Sales Order List', 'sos': sos, 'total_value': total_value, 'pending_orders': pending_orders, } return render(request, 'sales/so_list.html', context) from django.forms import inlineformset_factory from .models import SalesOrder, SalesOrderItem, Delivery, DeliveryItem class SalesOrderItemForm(forms.ModelForm): class Meta: model = SalesOrderItem 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 SalesOrderForm(forms.ModelForm): class Meta: model = SalesOrder fields = ['customer', '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}), } class DeliveryItemForm(forms.ModelForm): class Meta: model = DeliveryItem fields = ['so_item', 'delivered_quantity', 'notes'] widgets = { 'delivered_quantity': forms.NumberInput(attrs={'step': '0.01', 'min': '0'}), 'notes': forms.Textarea(attrs={'rows': 2}), } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if hasattr(self, 'so') and self.so: self.fields['so_item'].queryset = SalesOrderItem.objects.filter(so=self.so) class DeliveryForm(forms.ModelForm): class Meta: model = Delivery fields = ['delivery_number', 'delivery_date', 'notes'] widgets = { 'delivery_date': forms.DateInput(attrs={'type': 'date'}), 'notes': forms.Textarea(attrs={'rows': 3}), } SalesOrderItemFormSet = inlineformset_factory( SalesOrder, SalesOrderItem, form=SalesOrderItemForm, extra=0, can_delete=True, min_num=1, validate_min=True ) @login_required @permission_required('sales.add_salesorder', raise_exception=True) def create_so_view(request): """Create a new sales order""" if request.method == 'POST': form = SalesOrderForm(request.POST) formset = SalesOrderItemFormSet(request.POST) if form.is_valid() and formset.is_valid(): # Save the sales order so = form.save(commit=False) so.created_by = request.user so.save() # Save the items items = formset.save(commit=False) for item in items: item.so = so item.save() # Calculate totals so.subtotal = sum(item.total_price for item in so.items.all()) so.tax_amount = so.subtotal * Decimal('0.11') # 11% tax so.total_amount = so.subtotal + so.tax_amount so.save() messages.success(request, f'Sales Order "{so.so_number}" created successfully!') return redirect('sales:so_detail', so_number=so.so_number) else: form = SalesOrderForm() formset = SalesOrderItemFormSet() context = { 'form': form, 'formset': formset, 'module_title': 'Create Sales Order', 'is_create': True, } return render(request, 'sales/so_form.html', context) @login_required @permission_required('sales.view_salesorder', raise_exception=True) def so_detail_view(request, so_number): """View sales order details""" try: so = SalesOrder.objects.select_related('customer').prefetch_related('items').get(so_number=so_number) context = { 'module_title': 'Sales Order Details', 'so': so, } return render(request, 'sales/so_detail.html', context) except SalesOrder.DoesNotExist: messages.error(request, 'Sales order not found') return redirect('sales:so_list') @login_required @permission_required('sales.change_salesorder', raise_exception=True) def edit_so_view(request, so_number): """Edit sales order details""" so = get_object_or_404(SalesOrder, so_number=so_number) if request.method == 'POST': form = SalesOrderForm(request.POST, instance=so) formset = SalesOrderItemFormSet(request.POST, instance=so) # 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 sales order so = form.save(commit=False) so.save() # Save the items (excluding deleted ones) items = formset.save(commit=False) for item in items: item.so = so item.save() # Delete items marked for deletion for deleted_item in formset.deleted_objects: deleted_item.delete() # Recalculate totals so.subtotal = sum(item.total_price for item in so.items.all()) so.tax_amount = so.subtotal * Decimal('0.11') # 11% tax so.total_amount = so.subtotal + so.tax_amount so.save() messages.success(request, f'Sales Order "{so.so_number}" updated successfully!') return redirect('sales:so_detail', so_number=so.so_number) else: form = SalesOrderForm(instance=so) formset = SalesOrderItemFormSet(instance=so) context = { 'form': form, 'formset': formset, 'so': so, 'module_title': 'Edit Sales Order', 'is_create': False, } return render(request, 'sales/so_form.html', context) @login_required @permission_required('sales.change_salesorder', raise_exception=True) def approve_so_view(request, so_number): """Approve a sales order""" # In a real app, this would contain SO approval logic return HttpResponse("Approve sales order page") @login_required @permission_required('sales.change_salesorder', raise_exception=True) def cancel_so_view(request, so_number): """Cancel a sales order""" # In a real app, this would contain SO cancellation logic return HttpResponse("Cancel sales order page") @login_required @permission_required('sales.delete_salesorder', raise_exception=True) def delete_so_view(request, so_number): """Delete a sales order""" so = get_object_or_404(SalesOrder, so_number=so_number) if request.method == 'POST': so_number = so.so_number so.delete() messages.success(request, f'Sales Order "{so_number}" deleted successfully!') return redirect('sales:so_list') context = { 'so': so, 'module_title': 'Delete Sales Order', } return render(request, 'sales/so_confirm_delete.html', context) # Delivery Views @login_required @permission_required('sales.view_delivery', raise_exception=True) def delivery_list_view(request): """List all deliveries""" deliveries = Delivery.objects.select_related( 'so', 'so__customer', 'delivered_by' ).prefetch_related('items').order_by('-created_at') # Calculate statistics total_deliveries = deliveries.count() total_value = sum( sum(item.delivered_quantity * item.so_item.unit_price for item in delivery.items.all()) for delivery in deliveries ) context = { 'module_title': 'Delivery List', 'deliveries': deliveries, 'total_deliveries': total_deliveries, 'total_value': total_value, } return render(request, 'sales/delivery_list.html', context) @login_required @permission_required('sales.add_delivery', raise_exception=True) def create_delivery_view(request, so_number): """Create a new delivery""" so = get_object_or_404(SalesOrder, so_number=so_number) # Check if SO can have delivery if so.status not in ['processing', 'completed']: messages.error(request, f'Cannot create delivery for Sales Order in "{so.get_status_display()}" status.') return redirect('sales:so_detail', so_number=so.so_number) # Get undelivered items undelivered_items = so.items.filter( quantity__gt=F('shipped_quantity') ) if not undelivered_items: messages.warning(request, 'All items in this sales order have been fully delivered.') return redirect('sales:so_detail', so_number=so.so_number) if request.method == 'POST': # Generate delivery number (simple auto-increment) last_delivery = Delivery.objects.order_by('-id').first() delivery_number = f"DEL{str(last_delivery.id + 1).zfill(4)}" if last_delivery else "DEL0001" # Create delivery delivery = Delivery.objects.create( delivery_number=delivery_number, so=so, delivery_date=request.POST.get('delivery_date', timezone.now().date()), delivered_by=request.user, notes=request.POST.get('notes', '') ) # Create delivery items and update inventory for so_item in undelivered_items: # Convert Indonesian format (comma) to standard format (dot) for Decimal quantity_str = request.POST.get(f'quantity_{so_item.id}', '0').replace(',', '.') delivered_qty = Decimal(quantity_str) if quantity_str else Decimal('0') if delivered_qty > 0: # Check if delivery quantity doesn't exceed remaining quantity remaining_qty = so_item.quantity - so_item.shipped_quantity if delivered_qty > remaining_qty: delivered_qty = remaining_qty # Create delivery item DeliveryItem.objects.create( delivery=delivery, so_item=so_item, delivered_quantity=delivered_qty, notes=request.POST.get(f'notes_{so_item.id}', '') ) # Update SO item shipped quantity so_item.shipped_quantity += delivered_qty so_item.save() # Update inventory (decrease stock) warehouse = Warehouse.objects.first() # Default to first warehouse if not specified inventory, created = Inventory.objects.get_or_create( product=so_item.product, warehouse=warehouse, defaults={'quantity': 0} ) inventory.quantity -= delivered_qty inventory.save() # Create stock movement record StockMovement.objects.create( product=so_item.product, warehouse=warehouse, movement_type='out', quantity=delivered_qty, reference_number=f"DEL{delivery.delivery_number}", notes=f"Delivery from SO {so.so_number}", created_by=request.user ) # Check if SO is fully delivered total_shipped = sum(item.shipped_quantity for item in so.items.all()) total_ordered = sum(item.quantity for item in so.items.all()) if total_shipped >= total_ordered: so.status = 'completed' so.save() messages.success(request, f'Delivery "{delivery.delivery_number}" created successfully! Sales Order is now completed.') else: messages.success(request, f'Delivery "{delivery.delivery_number}" created successfully! Partial delivery recorded.') return redirect('sales:delivery_detail', delivery_number=delivery.delivery_number) context = { 'so': so, 'undelivered_items': undelivered_items, 'module_title': f'Create Delivery for {so.so_number}', 'today': timezone.now().date(), } return render(request, 'sales/delivery_form.html', context) @login_required @permission_required('sales.view_delivery', raise_exception=True) def delivery_detail_view(request, delivery_number): """View delivery details""" try: delivery = Delivery.objects.select_related('so', 'so__customer').prefetch_related('items', 'items__so_item', 'items__so_item__product').get(delivery_number=delivery_number) # Calculate totals total_quantity = sum(item.delivered_quantity for item in delivery.items.all()) total_value = sum(item.so_item.unit_price * item.delivered_quantity for item in delivery.items.all()) # Calculate previously delivered quantities and item totals for each item for item in delivery.items.all(): # Previously delivered = current total shipped - this delivery quantity item.previously_delivered = item.so_item.shipped_quantity - item.delivered_quantity # Calculate item total value item.item_total = item.so_item.unit_price * item.delivered_quantity context = { 'module_title': 'Delivery Details', 'delivery': delivery, 'total_quantity': total_quantity, 'total_value': total_value, } return render(request, 'sales/delivery_detail.html', context) except Delivery.DoesNotExist: messages.error(request, 'Delivery not found') return redirect('sales:delivery_list')