289 lines
12 KiB
Python
289 lines
12 KiB
Python
from django.shortcuts import render, get_object_or_404, redirect
|
|
from django.http import JsonResponse
|
|
from django.db import transaction
|
|
from django.db.models import Sum, F
|
|
from django.utils import timezone
|
|
from django.contrib import messages
|
|
from django.contrib.auth import login, logout, authenticate
|
|
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
|
|
from django.contrib.auth.decorators import login_required, user_passes_test
|
|
from django.contrib.auth.models import User
|
|
from .models import Ingredient, MenuItem, MenuItemIngredient, Order, OrderItem, UserProfile
|
|
import json
|
|
|
|
def is_manager(user):
|
|
return user.is_authenticated and hasattr(user, 'profile') and user.profile.role == 'manager'
|
|
|
|
def is_cashier(user):
|
|
return user.is_authenticated and hasattr(user, 'profile') and user.profile.role == 'cashier'
|
|
|
|
def manager_required(view_func):
|
|
return user_passes_test(is_manager, login_url='login')(view_func)
|
|
|
|
def cashier_or_manager_required(view_func):
|
|
return user_passes_test(lambda u: is_manager(u) or is_cashier(u), login_url='login')(view_func)
|
|
|
|
def login_view(request):
|
|
if request.method == 'POST':
|
|
form = AuthenticationForm(request, data=request.POST)
|
|
if form.is_valid():
|
|
user = form.get_user()
|
|
login(request, user)
|
|
if is_manager(user):
|
|
return redirect('dashboard')
|
|
return redirect('pos')
|
|
else:
|
|
form = AuthenticationForm()
|
|
return render(request, 'core/login.html', {'form': form})
|
|
|
|
def logout_view(request):
|
|
logout(request)
|
|
return redirect('login')
|
|
|
|
def home(request):
|
|
"""Redirect home to login or dashboard/pos based on role."""
|
|
if request.user.is_authenticated:
|
|
if is_manager(request.user):
|
|
return redirect('dashboard')
|
|
return redirect('pos')
|
|
return render(request, "core/index.html")
|
|
|
|
@cashier_or_manager_required
|
|
def pos_view(request):
|
|
"""Cashier POS interface."""
|
|
menu_items = MenuItem.objects.filter(is_active=True)
|
|
return render(request, "core/pos.html", {"menu_items": menu_items})
|
|
|
|
@cashier_or_manager_required
|
|
def create_order(request):
|
|
"""Handle order creation and stock deduction."""
|
|
if request.method == "POST":
|
|
try:
|
|
data = json.loads(request.body)
|
|
cart = data.get("cart", [])
|
|
notes = data.get("notes", "")
|
|
|
|
if not cart:
|
|
return JsonResponse({"success": False, "error": "Cart is empty"}, status=400)
|
|
|
|
with transaction.atomic():
|
|
order = Order.objects.create(customer_notes=notes)
|
|
total_price = 0
|
|
|
|
for item in cart:
|
|
menu_item = get_object_or_404(MenuItem, id=item["id"])
|
|
quantity = int(item["quantity"])
|
|
|
|
# Deduct stock
|
|
for recipe_item in menu_item.ingredients.all():
|
|
required_qty = recipe_item.quantity_required * quantity
|
|
ingredient = recipe_item.ingredient
|
|
if ingredient.stock_quantity < required_qty:
|
|
raise Exception(f"Insufficient stock for {ingredient.name}")
|
|
|
|
ingredient.stock_quantity = F('stock_quantity') - required_qty
|
|
ingredient.save()
|
|
|
|
# Create OrderItem
|
|
OrderItem.objects.create(
|
|
order=order,
|
|
menu_item=menu_item,
|
|
quantity=quantity,
|
|
price_at_order=menu_item.price
|
|
)
|
|
total_price += menu_item.price * quantity
|
|
|
|
order.total_price = total_price
|
|
order.save()
|
|
|
|
return JsonResponse({"success": True, "order_number": order.order_number})
|
|
except Exception as e:
|
|
return JsonResponse({"success": False, "error": str(e)}, status=400)
|
|
return JsonResponse({"success": False, "error": "Invalid request"}, status=405)
|
|
|
|
@cashier_or_manager_required
|
|
def receipt_view(request, order_number):
|
|
"""Printable receipt view."""
|
|
order = get_object_or_404(Order, order_number=order_number)
|
|
return render(request, "core/receipt.html", {"order": order})
|
|
|
|
@manager_required
|
|
def dashboard_view(request):
|
|
"""Manager dashboard for stock and reports."""
|
|
ingredients = Ingredient.objects.all()
|
|
# Summary of last 30 orders
|
|
orders = Order.objects.prefetch_related('items__menu_item').order_by('-created_at')[:50]
|
|
total_sales = Order.objects.aggregate(Sum('total_price'))['total_price__sum'] or 0
|
|
|
|
return render(request, "core/dashboard.html", {
|
|
"ingredients": ingredients,
|
|
"orders": orders,
|
|
"total_sales": total_sales,
|
|
})
|
|
|
|
@manager_required
|
|
def manage_users(request):
|
|
"""Manager only: Create and view accounts."""
|
|
users = User.objects.all().select_related('profile')
|
|
if request.method == 'POST':
|
|
username = request.POST.get('username')
|
|
password = request.POST.get('password')
|
|
role = request.POST.get('role')
|
|
|
|
if username and password and role:
|
|
if User.objects.filter(username=username).exists():
|
|
messages.error(request, f"User {username} already exists.")
|
|
else:
|
|
user = User.objects.create_user(username=username, password=password)
|
|
profile, created = UserProfile.objects.get_or_create(user=user)
|
|
profile.role = role
|
|
profile.save()
|
|
messages.success(request, f"User {username} created as {role}.")
|
|
return redirect('manage_users')
|
|
else:
|
|
messages.error(request, "Please fill all fields.")
|
|
|
|
return render(request, 'core/user_management.html', {'users': users})
|
|
|
|
@manager_required
|
|
def delete_user(request, user_id):
|
|
"""Manager only: Delete a user account."""
|
|
if request.user.id == user_id:
|
|
messages.error(request, "You cannot delete your own account.")
|
|
return redirect('manage_users')
|
|
|
|
user = get_object_or_404(User, id=user_id)
|
|
username = user.username
|
|
user.delete()
|
|
messages.success(request, f"User {username} has been deleted.")
|
|
return redirect('manage_users')
|
|
|
|
@manager_required
|
|
def manage_ingredients(request):
|
|
"""Manager only: View and add ingredients."""
|
|
if request.method == 'POST':
|
|
name = request.POST.get('name')
|
|
stock_quantity = request.POST.get('stock_quantity', 0)
|
|
unit = request.POST.get('unit', 'grams')
|
|
|
|
if name:
|
|
if Ingredient.objects.filter(name__iexact=name).exists():
|
|
messages.error(request, f"Ingredient '{name}' already exists.")
|
|
else:
|
|
Ingredient.objects.create(
|
|
name=name,
|
|
stock_quantity=stock_quantity,
|
|
unit=unit
|
|
)
|
|
messages.success(request, f"Ingredient '{name}' added successfully.")
|
|
else:
|
|
messages.error(request, "Ingredient name is required.")
|
|
return redirect('dashboard')
|
|
|
|
return redirect('dashboard')
|
|
|
|
@manager_required
|
|
def manage_menu(request):
|
|
"""Manager only: View and create menu items."""
|
|
menu_items = MenuItem.objects.all()
|
|
ingredients = Ingredient.objects.all()
|
|
|
|
if request.method == 'POST':
|
|
name = request.POST.get('name')
|
|
price = request.POST.get('price')
|
|
description = request.POST.get('description', '')
|
|
image_url = request.POST.get('image_url', '')
|
|
is_active = request.POST.get('is_active') == 'on'
|
|
|
|
# Handle ingredient IDs and quantities from the form
|
|
ingredient_ids = request.POST.getlist('ingredients')
|
|
quantities = request.POST.getlist('quantities')
|
|
|
|
if name and price:
|
|
try:
|
|
with transaction.atomic():
|
|
menu_item = MenuItem.objects.create(
|
|
name=name,
|
|
price=price,
|
|
description=description,
|
|
image_url=image_url,
|
|
is_active=is_active
|
|
)
|
|
|
|
# Add ingredients if provided
|
|
for i_id, qty in zip(ingredient_ids, quantities):
|
|
if i_id and qty and float(qty) > 0:
|
|
ingredient = get_object_or_404(Ingredient, id=i_id)
|
|
MenuItemIngredient.objects.create(
|
|
menu_item=menu_item,
|
|
ingredient=ingredient,
|
|
quantity_required=qty
|
|
)
|
|
|
|
messages.success(request, f"Menu item '{name}' created successfully.")
|
|
return redirect('manage_menu')
|
|
except Exception as e:
|
|
messages.error(request, f"Error creating menu item: {str(e)}")
|
|
else:
|
|
messages.error(request, "Name and price are required.")
|
|
|
|
return render(request, 'core/menu_management.html', {
|
|
'menu_items': menu_items,
|
|
'ingredients': ingredients
|
|
})
|
|
|
|
@manager_required
|
|
def edit_menu_item(request, pk):
|
|
"""Manager only: Edit an existing menu item."""
|
|
menu_item = get_object_or_404(MenuItem, pk=pk)
|
|
ingredients = Ingredient.objects.all()
|
|
|
|
if request.method == 'POST':
|
|
menu_item.name = request.POST.get('name')
|
|
menu_item.price = request.POST.get('price')
|
|
menu_item.description = request.POST.get('description', '')
|
|
menu_item.image_url = request.POST.get('image_url', '')
|
|
menu_item.is_active = request.POST.get('is_active') == 'on'
|
|
|
|
ingredient_ids = request.POST.getlist('ingredients')
|
|
quantities = request.POST.getlist('quantities')
|
|
|
|
if menu_item.name and menu_item.price:
|
|
try:
|
|
with transaction.atomic():
|
|
menu_item.save()
|
|
|
|
# Update ingredients: Clear existing and re-add
|
|
menu_item.ingredients.all().delete()
|
|
for i_id, qty in zip(ingredient_ids, quantities):
|
|
if i_id and qty and float(qty) > 0:
|
|
ingredient = get_object_or_404(Ingredient, id=i_id)
|
|
MenuItemIngredient.objects.create(
|
|
menu_item=menu_item,
|
|
ingredient=ingredient,
|
|
quantity_required=qty
|
|
)
|
|
|
|
messages.success(request, f"Menu item '{menu_item.name}' updated successfully.")
|
|
return redirect('manage_menu')
|
|
except Exception as e:
|
|
messages.error(request, f"Error updating menu item: {str(e)}")
|
|
else:
|
|
messages.error(request, "Name and price are required.")
|
|
|
|
return render(request, 'core/edit_menu_item.html', {
|
|
'menu_item': menu_item,
|
|
'ingredients': ingredients,
|
|
'current_ingredients': menu_item.ingredients.all()
|
|
})
|
|
|
|
@manager_required
|
|
def toggle_menu_item_status(request, pk):
|
|
"""Manager only: Toggle menu item active/inactive status."""
|
|
menu_item = get_object_or_404(MenuItem, pk=pk)
|
|
menu_item.is_active = not menu_item.is_active
|
|
menu_item.save()
|
|
status = "activated" if menu_item.is_active else "deactivated"
|
|
messages.success(request, f"Menu item '{menu_item.name}' {status}.")
|
|
return redirect('manage_menu')
|