diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index a5ed392..8c351d7 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index e061640..373d21b 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index 5a69659..a262fa5 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 2a36fd6..aaa26e7 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 8c38f3f..3bc3b7f 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,3 +1,34 @@ from django.contrib import admin +from .models import Ingredient, MenuItem, MenuItemIngredient, Order, OrderItem, UserProfile -# Register your models here. +@admin.register(UserProfile) +class UserProfileAdmin(admin.ModelAdmin): + list_display = ('user', 'role') + list_filter = ('role',) + search_fields = ('user__username',) + +class MenuItemIngredientInline(admin.TabularInline): + model = MenuItemIngredient + extra = 1 + +@admin.register(Ingredient) +class IngredientAdmin(admin.ModelAdmin): + list_display = ('name', 'stock_quantity', 'unit') + search_fields = ('name',) + +@admin.register(MenuItem) +class MenuItemAdmin(admin.ModelAdmin): + list_display = ('name', 'price', 'is_active') + inlines = [MenuItemIngredientInline] + search_fields = ('name',) + +class OrderItemInline(admin.TabularInline): + model = OrderItem + extra = 0 + readonly_fields = ('menu_item', 'quantity', 'price_at_order') + +@admin.register(Order) +class OrderAdmin(admin.ModelAdmin): + list_display = ('order_number', 'total_price', 'created_at') + inlines = [OrderItemInline] + readonly_fields = ('order_number', 'total_price', 'created_at', 'customer_notes') diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..5734ee3 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,65 @@ +# Generated by Django 5.2.7 on 2026-02-09 16:12 + +import django.db.models.deletion +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Ingredient', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, unique=True)), + ('stock_quantity', models.DecimalField(decimal_places=2, default=0.0, max_digits=10)), + ('unit', models.CharField(default='grams', max_length=20)), + ], + ), + migrations.CreateModel( + name='MenuItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, unique=True)), + ('price', models.DecimalField(decimal_places=2, max_digits=10)), + ('description', models.TextField(blank=True)), + ('image_url', models.URLField(blank=True, null=True)), + ('is_active', models.BooleanField(default=True)), + ], + ), + migrations.CreateModel( + name='Order', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('order_number', models.CharField(editable=False, max_length=12, unique=True)), + ('created_at', models.DateTimeField(default=django.utils.timezone.now)), + ('total_price', models.DecimalField(decimal_places=2, default=0.0, max_digits=10)), + ('customer_notes', models.TextField(blank=True)), + ], + ), + migrations.CreateModel( + name='MenuItemIngredient', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity_required', models.DecimalField(decimal_places=2, max_digits=10)), + ('ingredient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='core.ingredient')), + ('menu_item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ingredients', to='core.menuitem')), + ], + ), + migrations.CreateModel( + name='OrderItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('quantity', models.PositiveIntegerField(default=1)), + ('price_at_order', models.DecimalField(decimal_places=2, max_digits=10)), + ('menu_item', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='core.menuitem')), + ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='core.order')), + ], + ), + ] diff --git a/core/migrations/0002_userprofile.py b/core/migrations/0002_userprofile.py new file mode 100644 index 0000000..ef447d3 --- /dev/null +++ b/core/migrations/0002_userprofile.py @@ -0,0 +1,24 @@ +# Generated by Django 5.2.7 on 2026-02-09 16:25 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0001_initial'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='UserProfile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('role', models.CharField(choices=[('manager', 'Manager'), ('cashier', 'Cashier')], default='cashier', max_length=10)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)), + ], + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000..205652f Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/migrations/__pycache__/0002_userprofile.cpython-311.pyc b/core/migrations/__pycache__/0002_userprofile.cpython-311.pyc new file mode 100644 index 0000000..abf00b6 Binary files /dev/null and b/core/migrations/__pycache__/0002_userprofile.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 71a8362..d8d029a 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,65 @@ from django.db import models +from django.db import transaction +from django.utils import timezone +from django.contrib.auth.models import User +import uuid -# Create your models here. +class UserProfile(models.Model): + ROLE_CHOICES = [ + ('manager', 'Manager'), + ('cashier', 'Cashier'), + ] + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') + role = models.CharField(max_length=10, choices=ROLE_CHOICES, default='cashier') + + def __str__(self): + return f"{self.user.username} - {self.get_role_display()}" + +class Ingredient(models.Model): + name = models.CharField(max_length=100, unique=True) + stock_quantity = models.DecimalField(max_digits=10, decimal_places=2, default=0.00) + unit = models.CharField(max_length=20, default="grams") + + def __str__(self): + return f"{self.name} ({self.stock_quantity} {self.unit})" + +class MenuItem(models.Model): + name = models.CharField(max_length=100, unique=True) + price = models.DecimalField(max_digits=10, decimal_places=2) + description = models.TextField(blank=True) + image_url = models.URLField(blank=True, null=True) + is_active = models.BooleanField(default=True) + + def __str__(self): + return self.name + +class MenuItemIngredient(models.Model): + menu_item = models.ForeignKey(MenuItem, related_name='ingredients', on_delete=models.CASCADE) + ingredient = models.ForeignKey(Ingredient, on_delete=models.CASCADE) + quantity_required = models.DecimalField(max_digits=10, decimal_places=2) + + def __str__(self): + return f"{self.quantity_required} {self.ingredient.unit} of {self.ingredient.name} for {self.menu_item.name}" + +class Order(models.Model): + order_number = models.CharField(max_length=12, unique=True, editable=False) + created_at = models.DateTimeField(default=timezone.now) + total_price = models.DecimalField(max_digits=10, decimal_places=2, default=0.00) + customer_notes = models.TextField(blank=True) + + def save(self, *args, **kwargs): + if not self.order_number: + self.order_number = str(uuid.uuid4().hex[:8]).upper() + super().save(*args, **kwargs) + + def __str__(self): + return f"Order {self.order_number}" + +class OrderItem(models.Model): + order = models.ForeignKey(Order, related_name='items', on_delete=models.CASCADE) + menu_item = models.ForeignKey(MenuItem, on_delete=models.PROTECT) + quantity = models.PositiveIntegerField(default=1) + price_at_order = models.DecimalField(max_digits=10, decimal_places=2) + + def __str__(self): + return f"{self.quantity} x {self.menu_item.name}" diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..a14e57d 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,25 +1,101 @@ +{% load static %} - - - {% block title %}Knowledge Base{% endblock %} - {% if project_description %} - - - - {% endif %} - {% if project_image_url %} - - - {% endif %} - {% load static %} - - {% block head %}{% endblock %} + + + Searing Sandwiches | POS & Stock + + + + {% block extra_css %}{% endblock %} - - {% block content %}{% endblock %} - + +
+ {% block content %}{% endblock %} +
+ + + {% block extra_js %}{% endblock %} + diff --git a/core/templates/core/dashboard.html b/core/templates/core/dashboard.html new file mode 100644 index 0000000..baee4b5 --- /dev/null +++ b/core/templates/core/dashboard.html @@ -0,0 +1,79 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+

Manager Dashboard

+
+ Manage Users + Go to POS +
+
+ +
+
+
+
Total Sales (All Time)
+

${{ total_sales|stringformat:".2f" }}

+
+
+
+
+
Ingredient Stock Levels
+
+ {% for ingredient in ingredients %} +
+
+
{{ ingredient.name }}
+
{{ ingredient.stock_quantity|floatformat:0 }} {{ ingredient.unit }}
+
+
+ {% endfor %} +
+
+
+
+ +
+
+

Recent Orders

+
+ + + + + + + + + + + + + {% for order in orders %} + + + + + + + + + {% empty %} + + + + {% endfor %} + +
Order #TimeItemsNotesTotal
{{ order.order_number }}{{ order.created_at|date:"H:i" }} {{ order.created_at|date:"d M" }} + {% for item in order.items.all %} + {{ item.quantity }}x {{ item.menu_item.name }} + {% endfor %} + {{ order.customer_notes|truncatechars:30 }}${{ order.total_price }} + Receipt +
No orders found yet.
+
+
+
+
+{% endblock %} \ No newline at end of file diff --git a/core/templates/core/index.html b/core/templates/core/index.html index faec813..987c50e 100644 --- a/core/templates/core/index.html +++ b/core/templates/core/index.html @@ -1,145 +1,34 @@ -{% extends "base.html" %} - -{% block title %}{{ project_name }}{% endblock %} - -{% block head %} - - - - -{% endblock %} +{% extends 'base.html' %} +{% block title %}Welcome | Liver & Sausage POS{% endblock %} {% block content %} -
-
-

Analyzing your requirements and generating your app…

-
- Loading… +
+
+

+ Professional Cashier System +

+

+ Streamlined order management, automated stock tracking, and real-time reporting for your restaurant. +

+ +
+
+
+
+

Cashier POS

+

Create orders, customize sandwiches, and print receipts instantly.

+ Open POS +
+
+
+
+
+

Manager Dashboard

+

Track liver, sausage, and fries stock. View sales reports and manage prices.

+ View Reports +
+
+
-

AppWizzy AI is collecting your requirements and applying the first changes.

-

This page will refresh automatically as the plan is implemented.

-

- Runtime: Django {{ django_version }} · Python {{ python_version }} - — UTC {{ current_time|date:"Y-m-d H:i:s" }} -

-
-
- -{% endblock %} \ No newline at end of file + +{% endblock %} diff --git a/core/templates/core/login.html b/core/templates/core/login.html new file mode 100644 index 0000000..2f715b5 --- /dev/null +++ b/core/templates/core/login.html @@ -0,0 +1,32 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+
+
+

Login

+
+ {% csrf_token %} +
+ + +
+
+ + +
+
+ +
+
+ {% if form.errors %} +
+ Invalid username or password. +
+ {% endif %} +
+
+
+
+{% endblock %} diff --git a/core/templates/core/pos.html b/core/templates/core/pos.html new file mode 100644 index 0000000..70f9ed2 --- /dev/null +++ b/core/templates/core/pos.html @@ -0,0 +1,200 @@ +{% extends 'base.html' %} +{% block title %}POS | Cashier Interface{% endblock %} + +{% block extra_css %} +.menu-item-card { + cursor: pointer; + transition: all 0.2s; +} +.menu-item-card:hover { + border-color: var(--orange); + background-color: #2a2a2d; +} +.cart-sticky { + position: sticky; + top: 90px; + max-height: calc(100vh - 120px); + overflow-y: auto; +} +.cart-item { + border-bottom: 1px solid var(--slate); + padding: 10px 0; +} +.cart-item:last-child { + border-bottom: none; +} +{% endblock %} + +{% block content %} +
+ +
+

Menu

+
+ {% for item in menu_items %} +
+ +
+ {% endfor %} +
+
+ + +
+
+
+

Current Order

+
+
+
+

Cart is empty

+
+ +
+ + +
+ +
+ Subtotal + 0.00 EGP +
+
+

Total

+

0.00 EGP

+
+ + +
+
+
+
+ + + +{% endblock %} + +{% block extra_js %} + +{% endblock %} diff --git a/core/templates/core/receipt.html b/core/templates/core/receipt.html new file mode 100644 index 0000000..96e32fd --- /dev/null +++ b/core/templates/core/receipt.html @@ -0,0 +1,57 @@ + + + + + Receipt - {{ order.order_number }} + + + +
+ + +
+ +
+

LIVER & SAUSAGE

+

RESTAURANT POS

+

Order #: {{ order.order_number }}

+

{{ order.created_at|date:"Y-m-d H:i" }}

+
+ +
+ {% for item in order.items.all %} +
+ {{ item.quantity }} x {{ item.menu_item.name }} + {{ item.price_at_order }} +
+ {% endfor %} +
+ + + +
+

THANK YOU!

+
+ + diff --git a/core/templates/core/user_management.html b/core/templates/core/user_management.html new file mode 100644 index 0000000..824a859 --- /dev/null +++ b/core/templates/core/user_management.html @@ -0,0 +1,86 @@ +{% extends 'base.html' %} + +{% block content %} +
+
+

User Management

+ Back to Dashboard +
+ +
+ +
+
+
+

Create New Account

+
+ {% csrf_token %} +
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ {% if messages %} + {% for message in messages %} +
+ {{ message }} +
+ {% endfor %} + {% endif %} +
+
+
+ + +
+
+
+

Existing Accounts

+
+ + + + + + + + + + {% for user in users %} + + + + + + {% empty %} + + + + {% endfor %} + +
UsernameRoleDate Joined
{{ user.username }} + + {{ user.profile.role|title }} + + {{ user.date_joined|date:"M d, Y" }}
No users found.
+
+
+
+
+
+
+{% endblock %} diff --git a/core/urls.py b/core/urls.py index 6299e3d..98d3001 100644 --- a/core/urls.py +++ b/core/urls.py @@ -1,7 +1,13 @@ from django.urls import path - -from .views import home +from . import views urlpatterns = [ - path("", home, name="home"), + path('', views.home, name='home'), + path('login/', views.login_view, name='login'), + path('logout/', views.logout_view, name='logout'), + path('pos/', views.pos_view, name='pos'), + path('api/create-order/', views.create_order, name='create_order'), + path('receipt//', views.receipt_view, name='receipt'), + path('dashboard/', views.dashboard_view, name='dashboard'), + path('manage-users/', views.manage_users, name='manage_users'), ] diff --git a/core/views.py b/core/views.py index c9aed12..13d1dae 100644 --- a/core/views.py +++ b/core/views.py @@ -1,25 +1,146 @@ -import os -import platform - -from django import get_version as django_version -from django.shortcuts import render +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): - """Render the landing screen with loader and environment details.""" - host_name = request.get_host().lower() - agent_brand = "AppWizzy" if host_name == "appwizzy.com" else "Flatlogic" - now = timezone.now() + """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") - context = { - "project_name": "New Style", - "agent_brand": agent_brand, - "django_version": django_version(), - "python_version": platform.python_version(), - "current_time": now, - "host_name": host_name, - "project_description": os.getenv("PROJECT_DESCRIPTION", ""), - "project_image_url": os.getenv("PROJECT_IMAGE_URL", ""), - } - return render(request, "core/index.html", context) +@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})