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

528 lines
18 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.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')