# -*- coding: utf-8 -*- """ Populates the POS database with realistic demo data for customer presentations. Generates: - 1 manager account (username: manager / PIN: 1234 / password: password) - 4 demo waiters - 2 table groups with 12 tables total - A full menu: 3 categories, ~20 products - 45 days of backdated history (business days, shifts, orders, payments) Usage: python demo_seed.py Must be run from the local_backend directory. Designed to run on a clean DB (after wipe_database.py), but safe to run multiple times — checks for the [DEMO] marker before inserting anything. """ import json import random import sys from datetime import datetime, timedelta, timezone import bcrypt from sqlalchemy import text from database import engine, Base, SessionLocal import models.user import models.table import models.printer import models.product import models.order import models.business_day import models.shift import models.settings import models.flag import models.message from models.user import User, WaiterZone from models.table import TableGroup, Table from models.product import Category, Product from models.order import Order, OrderItem, OrderWaiter, OrderAuditLog from models.business_day import BusinessDay from models.shift import WaiterShift Base.metadata.create_all(bind=engine) # ── Config ──────────────────────────────────────────────────────────────────── DEMO_DAYS = 120 # how many past days to generate (~4 months) ORDERS_PER_DAY = (18, 35) # min/max orders per business day # ── Helpers ─────────────────────────────────────────────────────────────────── def _utc(dt: datetime) -> datetime: if dt.tzinfo is None: return dt.replace(tzinfo=timezone.utc) return dt def _hash_pin(pin: str) -> str: return bcrypt.hashpw(pin.encode(), bcrypt.gensalt()).decode() def _hash_password(pw: str) -> str: return bcrypt.hashpw(pw.encode(), bcrypt.gensalt()).decode() def _audit(db, order_id, event_type, waiter_id=None, item_ids=None, amount=None, payment_method=None, note=None, created_at=None): log = OrderAuditLog( order_id=order_id, event_type=event_type, waiter_id=waiter_id, item_ids=json.dumps(item_ids) if item_ids is not None else None, amount=amount, payment_method=payment_method, note=note, ) if created_at: log.created_at = _utc(created_at) db.add(log) # ── Menu definition ─────────────────────────────────────────────────────────── # (category_name, color, [(product_name, price), ...]) MENU = [ ("Ορεκτικά", "#f97316", [ ("Τζατζίκι", 4.50), ("Ταραμοσαλάτα", 4.50), ("Χωριάτικη Σαλάτα", 7.50), ("Κεφτεδάκια", 8.00), ("Σαγανάκι", 6.50), ("Χούμους", 5.00), ]), ("Κυρίως Πιάτα", "#ef4444", [ ("Μπριζόλα Χοιρινή", 12.00), ("Μπριζόλα Μοσχαρίσια", 16.00), ("Σουβλάκι Κοτόπουλο", 10.00), ("Μουσακάς", 11.00), ("Παστίτσιο", 10.00), ("Σπαγγέτι Μπολονέζ", 9.50), ("Γύρος Χοιρινός", 8.50), ]), ("Ποτά & Αναψυκτικά", "#3b82f6", [ ("Νερό 500ml", 1.00), ("Αναψυκτικό", 2.50), ("Μπύρα Φιάλη", 4.00), ("Κρασί (ποτήρι)", 4.50), ("Καφές Ελληνικός", 2.50), ("Φραπέ", 3.00), ("Χυμός Πορτοκάλι", 3.50), ]), ] # Weighted product picker: drinks ordered ~40% of the time, mains ~35%, starters ~25% CATEGORY_WEIGHTS = [0.25, 0.35, 0.40] WAITER_NAMES = [ ("Νίκος", "Νίκος Π."), ("Μαρία", "Μαρία Κ."), ("Πέτρος", "Πέτρος Α."), ("Έλενα", "Έλενα Μ."), ] PAYMENT_METHODS = ["cash", "cash", "cash", "card", "card"] # 60% cash / 40% card # ── Main ────────────────────────────────────────────────────────────────────── db = SessionLocal() try: # ── Guard: abort if demo data already present ───────────────────────────── existing = db.query(User).filter(User.full_name.like("[DEMO]%")).first() if existing: print("Demo data already present (found [DEMO] user). Nothing to do.") print("Run python wipe_database.py first if you want a fresh seed.") sys.exit(0) print("Creating demo data...") # ── Manager account ─────────────────────────────────────────────────────── manager = db.query(User).filter(User.username == "manager").first() if not manager: manager = User( username="manager", pin_hash=_hash_pin("1234"), password_hash=_hash_password("password"), role="manager", full_name="Manager", is_active=True, ) db.add(manager) db.flush() print(" created manager (username: manager / PIN: 1234 / password: password)") else: # Ensure password_hash is set on existing manager if not manager.password_hash: manager.password_hash = _hash_password("password") db.flush() print(" manager account already exists — updated password to 'password'") # ── Waiters ─────────────────────────────────────────────────────────────── waiters = [] for nickname, full_name in WAITER_NAMES: username = f"demo_{nickname.lower()}" w = db.query(User).filter(User.username == username).first() if not w: w = User( username=username, pin_hash=_hash_pin(str(random.randint(1000, 9999))), role="waiter", full_name=f"[DEMO] {full_name}", nickname=nickname, is_active=True, ) db.add(w) db.flush() waiters.append(w) print(f" created {len(waiters)} demo waiters") # ── Table groups & tables ───────────────────────────────────────────────── group_configs = [ ("Κεντρική Αίθουσα", "Κ", "#6366f1", list(range(1, 9))), # tables 1-8 ("Βεράντα", "Β", "#22c55e", list(range(1, 6))), # tables 1-5 ] all_tables = [] for g_name, g_prefix, g_color, table_numbers in group_configs: grp = db.query(TableGroup).filter(TableGroup.name == g_name).first() if not grp: grp = TableGroup(name=g_name, prefix=g_prefix, color=g_color, sort_order=len(all_tables)) db.add(grp) db.flush() for n in table_numbers: tbl = db.query(Table).filter(Table.group_id == grp.id, Table.number == n).first() if not tbl: tbl = Table(number=n, group_id=grp.id, is_active=True) db.add(tbl) db.flush() all_tables.append(tbl) print(f" created {len(all_tables)} tables across {len(group_configs)} groups") # ── Waiter zone assignments (all waiters → all zones) ───────────────────── for w in waiters: existing_zone = db.query(WaiterZone).filter( WaiterZone.waiter_id == w.id, WaiterZone.group_id.is_(None) ).first() if not existing_zone: db.add(WaiterZone(waiter_id=w.id, group_id=None)) db.flush() # ── Menu ────────────────────────────────────────────────────────────────── categories = [] all_products = [] # flat list for random picks category_product_map = [] # parallel list of product sub-lists (for weighted pick) for sort_idx, (cat_name, cat_color, items) in enumerate(MENU): cat = db.query(Category).filter(Category.name == cat_name).first() if not cat: cat = Category(name=cat_name, color=cat_color, sort_order=sort_idx) db.add(cat) db.flush() categories.append(cat) cat_products = [] for prod_sort, (prod_name, prod_price) in enumerate(items): p = db.query(Product).filter(Product.name == prod_name, Product.category_id == cat.id).first() if not p: p = Product( name=prod_name, category_id=cat.id, base_price=prod_price, is_available=True, lifecycle_status="active", sort_order=prod_sort, ) db.add(p) db.flush() cat_products.append(p) all_products.append(p) category_product_map.append(cat_products) print(f" created {len(categories)} categories with {len(all_products)} products") db.commit() # ── Historical days ─────────────────────────────────────────────────────── today = datetime.now(timezone.utc).date() total_orders = 0 total_revenue = 0.0 print(f"\nGenerating {DEMO_DAYS} days of history...") for days_ago in range(DEMO_DAYS, 0, -1): day_date = today - timedelta(days=days_ago) # Business day: opens at ~10:00, closes at ~23:30 open_time = _utc(datetime(day_date.year, day_date.month, day_date.day, 10, random.randint(0, 30))) close_time = _utc(datetime(day_date.year, day_date.month, day_date.day, 23, random.randint(15, 45))) bday = BusinessDay( status="closed", opened_at=open_time, opened_by_id=manager.id, closed_at=close_time, closed_by_id=manager.id, ) db.add(bday) db.flush() # 2-4 waiters work each day working_waiters = random.sample(waiters, k=random.randint(2, len(waiters))) STARTING_CASH_OPTIONS = [50.0, 80.0, 100.0, 110.0, 120.0, 150.0, 200.0] shifts = {} shift_totals = {} # shift_id → running revenue total, filled during order loop for w in working_waiters: shift_start = open_time + timedelta(minutes=random.randint(0, 30)) shift_end = close_time - timedelta(minutes=random.randint(0, 20)) shift = WaiterShift( waiter_id=w.id, business_day_id=bday.id, started_at=shift_start, ended_at=shift_end, starting_cash=random.choice(STARTING_CASH_OPTIONS), ) db.add(shift) db.flush() shifts[w.id] = shift shift_totals[shift.id] = 0.0 # Orders — two rushes: lunch (12-15h) and dinner (19-23h) n_orders = random.randint(*ORDERS_PER_DAY) lunch_count = int(n_orders * 0.4) dinner_count = n_orders - lunch_count order_times = [] for _ in range(lunch_count): h = random.randint(12, 14) m = random.randint(0, 59) order_times.append(datetime(day_date.year, day_date.month, day_date.day, h, m)) for _ in range(dinner_count): h = random.randint(19, 22) m = random.randint(0, 59) order_times.append(datetime(day_date.year, day_date.month, day_date.day, h, m)) order_times.sort() # Track which tables are "busy" at each point in time so we don't double-seat # Format: {table_id: release_time} table_busy_until: dict[int, datetime] = {} for opened_at_naive in order_times: opened_at = _utc(opened_at_naive) # Pick a free table free_tables = [ t for t in all_tables if table_busy_until.get(t.id, datetime.min.replace(tzinfo=timezone.utc)) <= opened_at ] if not free_tables: free_tables = all_tables # all busy → just reuse (edge case) table = random.choice(free_tables) # Pick a waiter who is working today waiter = random.choice(working_waiters) shift = shifts[waiter.id] # Order duration: 20-70 min duration_min = random.randint(20, 70) closed_at = opened_at + timedelta(minutes=duration_min) # Cap to close_time if closed_at > close_time: closed_at = close_time - timedelta(minutes=2) table_busy_until[table.id] = closed_at order = Order( table_id=table.id, opened_by=waiter.id, business_day_id=bday.id, status="closed", closed_by=manager.id, notes=None, source="pos", ) order.opened_at = opened_at order.closed_at = closed_at db.add(order) db.flush() db.add(OrderWaiter( order_id=order.id, waiter_id=waiter.id, assigned_at=opened_at, )) _audit(db, order.id, "ORDER_OPENED", waiter_id=waiter.id, created_at=opened_at) # Items: 2-6 items, weighted by category n_items = random.randint(2, 6) chosen_category_lists = random.choices(category_product_map, weights=CATEGORY_WEIGHTS, k=n_items) items_added = [] order_total = 0.0 items_added_at = opened_at + timedelta(minutes=random.randint(2, 8)) for prod_list in chosen_category_lists: product = random.choice(prod_list) qty = random.choices([1, 2, 3], weights=[0.7, 0.2, 0.1])[0] payment_method = random.choice(PAYMENT_METHODS) paid_at = closed_at - timedelta(minutes=random.randint(1, 5)) line_total = product.base_price * qty order_total += line_total item = OrderItem( order_id=order.id, product_id=product.id, added_by=waiter.id, quantity=qty, unit_price=product.base_price, status="paid", printed=True, paid_by=waiter.id, paid_at=_utc(paid_at), payment_method=payment_method, paid_in_shift_id=shift.id, ) item.added_at = _utc(items_added_at) db.add(item) db.flush() items_added.append(item.id) shift_totals[shift.id] = round(shift_totals[shift.id] + line_total, 2) _audit(db, order.id, "ITEMS_ADDED", waiter_id=waiter.id, item_ids=items_added, created_at=items_added_at) # Payment audit (single payment per order for simplicity) payment_method = random.choice(PAYMENT_METHODS) pay_at = closed_at - timedelta(minutes=1) _audit(db, order.id, "PAYMENT", waiter_id=waiter.id, item_ids=items_added, amount=round(order_total, 2), payment_method=payment_method, created_at=pay_at) _audit(db, order.id, "ORDER_CLOSED", waiter_id=waiter.id, created_at=closed_at) total_orders += 1 total_revenue += order_total # Write total_collected snapshot onto each ended shift for w in working_waiters: s = shifts[w.id] s.total_collected = shift_totals[s.id] db.commit() print(f" {day_date} {n_orders:2d} orders {len(working_waiters)} waiters") print(f"\nDone.") print(f" Total orders : {total_orders}") print(f" Total revenue : €{total_revenue:,.2f}") print(f"\nLogin credentials:") print(f" Manager — username: manager | password: password | PIN: 1234") for w in waiters: print(f" Waiter — username: {w.username}") finally: db.close()