695 lines
25 KiB
Python
695 lines
25 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, 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)
|