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

569 lines
20 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
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')