Files
xenia-pos-local/local_backend/demo_seed.py
bonamin 34ae328b0d feat: bump client-services (accumulated feature work + deploy fixes)
Snapshot of in-progress work across local_backend, manager_dashboard,
and waiter_pwa (pricing, chat, fiscal, prep zones, recovery codes, CRM,
inventory, permissions), plus the nginx/docker-compose deploy fixes for
the Unraid + NPM reverse-proxy setup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-19 10:00:14 +03:00

879 lines
38 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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 waiters with real Greek names
- 2 table groups with 13 tables total
- A full menu: 3 categories, ~20 products with costs
- 120 days of backdated history with realistic time distribution
- Online orders (source="online") scattered through history
- Order cancellations (~10% of items)
- Site notes and todos
- Contacts (suppliers) and expenses over the period
- Customers and tabs
- Scheduled shifts for the next 3 weeks
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 guard before inserting anything.
"""
import json
import random
import sys
from datetime import datetime, timedelta, date, 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.customers # noqa: F401 — must be before models.order (orders.customer_id FK)
import models.order
import models.business_day
import models.shift
import models.settings
import models.flag
import models.message
import models.notes # noqa: F401 — registers tables with Base.metadata
import models.expenses # noqa: F401
import models.tabs # noqa: F401
import models.schedule # noqa: F401
import models.waste # noqa: F401
import models.reservation # noqa: F401
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
from models.notes import SiteNote, SiteTodo
from models.expenses import Contact, Expense, ExpensePayment
from models.customers import Customer
from models.tabs import Tab, TabEntry, TabPayment
from models.schedule import ScheduledShift
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)
def _random_time_in_day(day_date: date, hour_weights: list) -> datetime:
"""
Returns a random datetime in day_date, biased toward peak hours.
hour_weights: list of (hour, weight) tuples covering the operating hours.
"""
hours = [h for h, _ in hour_weights]
weights = [w for _, w in hour_weights]
h = random.choices(hours, weights=weights)[0]
m = random.randint(0, 59)
return datetime(day_date.year, day_date.month, day_date.day, h, m)
# Operating hours weighted toward lunch (12-14) and dinner peaks (19-22),
# with lighter morning coffee traffic (10-11) and afternoon lulls (15-18).
HOUR_WEIGHTS = [
(10, 3), (11, 5),
(12, 12), (13, 15), (14, 10),
(15, 4), (16, 3), (17, 4),
(18, 6), (19, 14), (20, 16), (21, 13), (22, 8),
]
# Day-of-week multipliers (Mon=0 … Sun=6) — weekends busier
DOW_MULTIPLIER = {0: 0.75, 1: 0.75, 2: 0.80, 3: 0.85, 4: 1.00, 5: 1.20, 6: 1.10}
# ── Menu definition ───────────────────────────────────────────────────────────
# (category_name, color, [(product_name, price, cost), ...])
MENU = [
("Ορεκτικά", "#f97316", [
("Τζατζίκι", 4.50, 1.20),
("Ταραμοσαλάτα", 4.50, 1.10),
("Χωριάτικη Σαλάτα",7.50, 2.00),
("Κεφτεδάκια", 8.00, 2.80),
("Σαγανάκι", 6.50, 2.20),
("Χούμους", 5.00, 1.40),
]),
("Κυρίως Πιάτα", "#ef4444", [
("Μπριζόλα Χοιρινή", 12.00, 4.50),
("Μπριζόλα Μοσχαρίσια", 16.00, 7.00),
("Σουβλάκι Κοτόπουλο", 10.00, 3.20),
("Μουσακάς", 11.00, 3.80),
("Παστίτσιο", 10.00, 3.50),
("Σπαγγέτι Μπολονέζ", 9.50, 3.00),
("Γύρος Χοιρινός", 8.50, 2.60),
]),
("Ποτά & Αναψυκτικά", "#3b82f6", [
("Νερό 500ml", 1.00, 0.20),
("Αναψυκτικό", 2.50, 0.60),
("Μπύρα Φιάλη", 4.00, 1.20),
("Κρασί (ποτήρι)", 4.50, 1.00),
("Καφές Ελληνικός", 2.50, 0.45),
("Φραπέ", 3.00, 0.50),
("Χυμός Πορτοκάλι", 3.50, 0.90),
]),
]
# Weighted product picker: drinks ordered ~40% of the time, mains ~35%, starters ~25%
CATEGORY_WEIGHTS = [0.25, 0.35, 0.40]
WAITER_NAMES = [
("Νίκος", "Νίκος Παπαδόπουλος", "demo_nikos", "2201"),
("Μαρία", "Μαρία Κωνσταντίνου", "demo_maria", "3302"),
("Πέτρος", "Πέτρος Αναστασίου", "demo_petros", "4403"),
("Έλενα", "Έλενα Μιχαηλίδου", "demo_elena", "5504"),
]
PAYMENT_METHODS = ["cash", "cash", "cash", "card", "card"] # 60% cash / 40% card
CANCEL_REASONS = [
"Λάθος παραγγελία",
"Ο πελάτης άλλαξε γνώμη",
"Το προϊόν δεν είναι διαθέσιμο",
"Λάθος τραπέζι",
]
# ── Main ──────────────────────────────────────────────────────────────────────
db = SessionLocal()
try:
# ── Guard: abort if demo data already present ─────────────────────────────
existing = db.query(User).filter(User.username == "demo_nikos").first()
if existing:
print("Demo data already present (found demo_nikos 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:
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, username, pin in WAITER_NAMES:
w = db.query(User).filter(User.username == username).first()
if not w:
w = User(
username=username,
pin_hash=_hash_pin(pin),
role="waiter",
full_name=full_name,
nickname=nickname,
is_active=True,
)
db.add(w)
db.flush()
waiters.append(w)
print(f" created {len(waiters)} 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 ───────────────────────────────────────────────
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 = []
category_product_map = []
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, prod_cost) 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,
cost_simple=prod_cost,
is_available=True,
lifecycle_status="active",
sort_order=prod_sort,
)
db.add(p)
db.flush()
else:
p.cost_simple = prod_cost
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 (with costs)")
# ── Contacts (suppliers & utilities) ─────────────────────────────────────
contact_defs = [
("Κρεοπωλείο Παπαδόπουλος", "supplier", "+30 22310 41234", None, "Πιερία 12, Κατερίνη", "Κύριος προμηθευτής κρέατος"),
("Αλιεύματα Κόκκαλης", "supplier", "+30 22310 55678", None, "Λιμάνι, Κατερίνη", "Φρέσκα ψάρια και θαλασσινά"),
("Χονδρικό Ελαιόλαδο", "supplier", "+30 23310 22100", "oil@agrofarm.gr","Αγροτική Οδός 5", "Ελαιόλαδο και τυρί χύμα"),
("Αναψυκτικά ΑΕΒΕ", "supplier", "+30 210 9012345", "orders@aeve.gr", "Βιομηχανική Ζώνη, Αθήνα","Αναψυκτικά, νερά, μπύρες"),
("ΔΕΗ", "utility", "11500", None, None, "Ηλεκτρισμός"),
("ΕΥΔΑΠ / Τοπικό Δίκτυο", "utility", "11888", None, None, "Νερό"),
("Cosmote Επαγγελματικό", "utility", "13888", None, None, "Τηλεφωνία & internet"),
]
contacts = []
for c_name, c_type, c_phone, c_email, c_addr, c_notes in contact_defs:
c = db.query(Contact).filter(Contact.name == c_name).first()
if not c:
c = Contact(
name=c_name,
type=c_type,
phone=c_phone,
email=c_email,
address=c_addr,
notes=c_notes,
is_active=True,
)
db.add(c)
db.flush()
contacts.append(c)
print(f" created {len(contacts)} contacts")
# ── Customers ─────────────────────────────────────────────────────────────
customer_defs = [
("Γιώργος Σταθόπουλος", "Γιώργης", "+30 6944 111222", None, "Τακτικός πελάτης, κάθε Παρασκευή βράδυ"),
("Ελευθερία Νικολάου", "Ελευθερία","+30 6977 334455", "eleftheria@gmail.com", "Αλλεργία στη γλουτένη"),
("Δημήτρης Παππάς", "Μήτσος", "+30 6955 667788", None, "Προτιμά τραπέζι στη βεράντα"),
("Σοφία Αλεξίου", "Σοφία", "+30 6933 998877", "sofia.alex@hotmail.com", "VIP πελάτης"),
("Ομάδα Εργατών", None, "+30 6900 123456", None, "Ομάδα οικοδόμων — συχνά μεσημέρι"),
]
customers = []
for c_name, c_nick, c_phone, c_email, c_notes in customer_defs:
c = db.query(Customer).filter(Customer.name == c_name).first()
if not c:
c = Customer(
name=c_name,
nickname=c_nick,
phone=c_phone,
email=c_email,
notes=c_notes,
is_active=True,
created_by_id=manager.id,
)
db.add(c)
db.flush()
customers.append(c)
print(f" created {len(customers)} customers")
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...")
# Keep track of created business days (date → BusinessDay) for expense linking
bday_by_date: dict[date, BusinessDay] = {}
for days_ago in range(DEMO_DAYS, 0, -1):
day_date = today - timedelta(days=days_ago)
dow = day_date.weekday()
multiplier = DOW_MULTIPLIER[dow]
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()
bday_by_date[day_date] = bday
# 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 = {}
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 distributed realistically across the day
base_n = random.randint(*ORDERS_PER_DAY)
n_orders = max(5, int(base_n * multiplier))
# ~8% of days have 1-2 online orders
n_online = random.choices([0, 1, 2], weights=[0.60, 0.28, 0.12])[0]
n_pos = n_orders - n_online
order_times = []
for _ in range(n_pos):
order_times.append(_random_time_in_day(day_date, HOUR_WEIGHTS))
order_times.sort()
# Online orders land at lunch or dinner time
online_order_times = []
for _ in range(n_online):
h = random.choices([12, 13, 19, 20, 21], weights=[2, 2, 3, 3, 2])[0]
online_order_times.append(datetime(day_date.year, day_date.month, day_date.day,
h, random.randint(0, 59)))
table_busy_until: dict[int, datetime] = {}
# ── POS orders ────────────────────────────────────────────────────────
for opened_at_naive in order_times:
opened_at = _utc(opened_at_naive)
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
table = random.choice(free_tables)
waiter = random.choice(working_waiters)
shift = shifts[waiter.id]
duration_min = random.randint(20, 70)
closed_at = opened_at + timedelta(minutes=duration_min)
if closed_at > close_time:
closed_at = close_time - timedelta(minutes=2)
table_busy_until[table.id] = closed_at
# ~15% of orders linked to a known customer
order_customer = random.choice(customers) if random.random() < 0.15 else None
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",
customer_id=order_customer.id if order_customer else None,
)
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)
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]
# ~10% chance the item gets cancelled
is_cancelled = random.random() < 0.10
paid_at = closed_at - timedelta(minutes=random.randint(1, 5))
line_total = product.base_price * qty
item = OrderItem(
order_id=order.id,
product_id=product.id,
added_by=waiter.id,
quantity=qty,
unit_price=product.base_price,
unit_cost=product.cost_simple,
status="cancelled" if is_cancelled else "paid",
printed=True,
)
if is_cancelled:
cancel_at = items_added_at + timedelta(minutes=random.randint(1, 10))
item.cancelled_by = waiter.id
item.cancelled_at = _utc(cancel_at)
item.cancel_reason = random.choice(CANCEL_REASONS)
else:
payment_method = random.choice(PAYMENT_METHODS)
item.paid_by = waiter.id
item.paid_at = _utc(paid_at)
item.payment_method = payment_method
item.paid_in_shift_id = shift.id
order_total += line_total
shift_totals[shift.id] = round(shift_totals[shift.id] + line_total, 2)
item.added_at = _utc(items_added_at)
db.add(item)
db.flush()
items_added.append(item.id)
_audit(db, order.id, "ITEMS_ADDED",
waiter_id=waiter.id, item_ids=items_added, created_at=items_added_at)
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
# ── Online orders ─────────────────────────────────────────────────────
online_ref_counter = days_ago * 10 # rough unique ref per day
for ot in online_order_times:
opened_at = _utc(ot)
duration_min = random.randint(25, 50)
closed_at = opened_at + timedelta(minutes=duration_min)
if closed_at > close_time:
closed_at = close_time - timedelta(minutes=2)
online_type = random.choice(["delivery", "dine_in"])
online_names = ["Αλέξης Κ.", "Μαρία Π.", "Νίκος Σ.", "Αθηνά Λ.", "Θανάσης Β.", "Γιώτα Δ."]
online_phones = ["+30 694 111 0000", "+30 697 222 1111", "+30 693 333 2222",
"+30 699 444 3333", "+30 698 555 4444"]
o_name = random.choice(online_names)
o_phone = random.choice(online_phones)
o_status = random.choices(
["delivered", "delivered", "delivered", "rejected"],
weights=[0.85, 0.05, 0.05, 0.05]
)[0]
ref = f"ORD-{online_ref_counter:04d}"
online_ref_counter += 1
order = Order(
table_id=None,
opened_by=manager.id,
business_day_id=bday.id,
status="closed" if o_status == "delivered" else "cancelled",
closed_by=manager.id,
source="online",
online_order_ref=ref,
online_status=o_status,
online_customer_name=o_name,
online_customer_phone=o_phone,
online_order_type=online_type,
online_customer_notes=random.choice([None, None, "Χωρίς κρεμμύδι", "Γρήγορα παρακαλώ", None]),
order_type="delivery" if online_type == "delivery" else "here",
)
order.opened_at = opened_at
order.closed_at = closed_at
db.add(order)
db.flush()
_audit(db, order.id, "ORDER_OPENED", waiter_id=manager.id, created_at=opened_at)
n_items = random.randint(1, 4)
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=2)
for prod_list in chosen_category_lists:
product = random.choice(prod_list)
qty = random.choices([1, 2], weights=[0.8, 0.2])[0]
line_total = product.base_price * qty
order_total += line_total
item = OrderItem(
order_id=order.id,
product_id=product.id,
added_by=manager.id,
quantity=qty,
unit_price=product.base_price,
unit_cost=product.cost_simple,
status="paid" if o_status == "delivered" else "cancelled",
printed=True,
paid_by=manager.id if o_status == "delivered" else None,
paid_at=_utc(closed_at) if o_status == "delivered" else None,
payment_method="card" if o_status == "delivered" else None,
)
item.added_at = _utc(items_added_at)
db.add(item)
db.flush()
items_added.append(item.id)
if o_status == "delivered":
_audit(db, order.id, "PAYMENT",
waiter_id=manager.id, item_ids=items_added,
amount=round(order_total, 2), payment_method="card",
created_at=closed_at - timedelta(minutes=1))
total_revenue += order_total
_audit(db, order.id, "ORDER_CLOSED", waiter_id=manager.id, created_at=closed_at)
total_orders += 1
# Write total_collected onto shifts
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 (dow={dow})")
# ── Expenses ──────────────────────────────────────────────────────────────
print("\nAdding expenses...")
supplier_meat = contacts[0]
supplier_fish = contacts[1]
supplier_olive = contacts[2]
supplier_bev = contacts[3]
util_power = contacts[4]
util_water = contacts[5]
util_phone = contacts[6]
def _add_expense(description, category, contact, amount, paid_amount,
days_back, notes=None):
exp_date = today - timedelta(days=days_back)
bday_obj = bday_by_date.get(exp_date)
due = _utc(datetime(exp_date.year, exp_date.month, exp_date.day, 9, 0))
exp = Expense(
description=description,
category=category,
contact_id=contact.id if contact else None,
total_amount=amount,
paid_amount=paid_amount,
due_date=due,
business_day_id=bday_obj.id if bday_obj else None,
created_by_id=manager.id,
notes=notes,
)
exp.created_at = due
db.add(exp)
db.flush()
if paid_amount > 0:
pmt = ExpensePayment(
expense_id=exp.id,
amount=paid_amount,
paid_by_id=manager.id,
notes=None,
)
pmt.paid_at = due + timedelta(hours=1)
db.add(pmt)
db.flush()
# Recurring weekly: meat supplier (~every 5 days)
for i in range(0, DEMO_DAYS, 5):
_add_expense("Κρέας εβδομάδας", "supplier", supplier_meat,
round(random.uniform(180, 280), 2), 0,
DEMO_DAYS - i, "Παράδοση Δευτέρα πρωί")
# Recurring bi-weekly: fish
for i in range(0, DEMO_DAYS, 10):
_add_expense("Ψάρια & θαλασσινά", "supplier", supplier_fish,
round(random.uniform(90, 160), 2), 0,
DEMO_DAYS - i)
# Olive oil once a month
for i in range(0, DEMO_DAYS, 30):
_add_expense("Ελαιόλαδο & τυρί", "supplier", supplier_olive,
round(random.uniform(120, 200), 2), 0,
DEMO_DAYS - i)
# Beverages every 2 weeks
for i in range(0, DEMO_DAYS, 14):
amt = round(random.uniform(200, 350), 2)
_add_expense("Αναψυκτικά & μπύρες", "supplier", supplier_bev,
amt, amt, # fully paid
DEMO_DAYS - i)
# Monthly utility bills
for month_offset in range(4):
days_back = 30 * (month_offset + 1)
_add_expense("ΔΕΗ — μηνιαίος λογαριασμός", "utilities", util_power,
round(random.uniform(280, 420), 2), 0, days_back)
_add_expense("Νερό — μηνιαίος λογαριασμός", "utilities", util_water,
round(random.uniform(45, 90), 2), 0, days_back + 2)
_add_expense("Τηλεφωνία & Internet", "utilities", util_phone,
49.90, 49.90, days_back + 1)
# Rent once a month
for month_offset in range(4):
days_back = 30 * (month_offset + 1) - 5
_add_expense("Ενοίκιο καταστήματος", "rent", None,
1200.0, 1200.0, days_back,
"Πάντα στις αρχές του μήνα")
# One maintenance expense
_add_expense("Επισκευή ψυγείου", "maintenance", None,
320.0, 320.0, 45, "Συμπιεστής — Τεχνίτης Σωτήρης")
db.commit()
print(" expenses done")
# ── Tabs ──────────────────────────────────────────────────────────────────
print("Adding tabs...")
def _add_tab(customer, entries_desc, payments, is_closed=False):
"""entries_desc: list of (amount, description). payments: list of amounts."""
tab_opened = _utc(datetime.now(timezone.utc) - timedelta(days=random.randint(5, 30)))
tab = Tab(
customer_id=customer.id,
status="closed" if is_closed else "open",
opened_at=tab_opened,
closed_by_id=manager.id if is_closed else None,
closed_at=tab_opened + timedelta(days=random.randint(3, 15)) if is_closed else None,
notes=None,
)
db.add(tab)
db.flush()
entry_time = tab_opened
for amount, desc in entries_desc:
entry_time += timedelta(days=random.randint(1, 3))
e = TabEntry(
tab_id=tab.id,
order_id=None,
order_item_id=None,
amount=amount,
description=desc,
created_by_id=manager.id,
)
e.created_at = entry_time
db.add(e)
pay_time = entry_time + timedelta(days=1)
for pay_amount in payments:
pay_time += timedelta(hours=random.randint(1, 24))
p = TabPayment(
tab_id=tab.id,
amount=pay_amount,
payment_method=random.choice(["cash", "card"]),
received_by_id=manager.id,
notes=None,
)
p.created_at = pay_time
db.add(p)
db.flush()
# Open tab: Γιώργης hasn't settled his tab yet
_add_tab(customers[0], [
(35.50, "Δείπνο 3 ατόμων — 15/05"),
(28.00, "Μεσημεριανό — 22/05"),
(42.00, "Δείπνο 4 ατόμων — 28/05"),
], [20.00]) # partial payment
# Closed tab: Σοφία settled hers
_add_tab(customers[3], [
(55.00, "Εταιρικό δείπνο — 10/04"),
(30.00, "Μεσημεριανό — 18/04"),
], [85.00], is_closed=True)
# Open tab: Ομάδα εργατών — running tab
_add_tab(customers[4], [
(22.00, "Μεσημεριανό ομάδα — 01/06"),
(25.50, "Μεσημεριανό ομάδα — 05/06"),
(19.00, "Μεσημεριανό ομάδα — 09/06"),
], [30.00])
db.commit()
print(" tabs done")
# ── Site Notes & Todos ────────────────────────────────────────────────────
print("Adding notes & todos...")
notes_data = [
("Ο φούρνος χρειάζεται σέρβις — να κλείσουμε ραντεβού με τεχνίτη.", True),
("Νέα τιμή μπύρας από 1 Ιουλίου: Heineken 4.50€ (από 4.00€).", False),
("Παραγγελία ελαιολάδου: 10 κιλά εξαιρετικό παρθένο από Αλεξόπουλο.", False),
("ΔΕΗ: η επόμενη πληρωμή λήγει στις 20 Ιουλίου. Να μην ξεχαστεί!", True),
("Το κλιματιστικό βεράντας λειτουργεί πάλι μετά την επισκευή (Παρ. 07/06).", False),
]
for body, pinned in notes_data:
note = SiteNote(
body=body,
created_by_id=manager.id,
is_pinned=pinned,
)
db.add(note)
todos_data = [
("Ανανέωση τιμοκαταλόγου για καλοκαίρι", "high", False),
("Αγορά νέων ποτηριών κρασιού (χάλασαν 4)", "normal", False),
("Έλεγχος πυροσβεστήρων — Ιούλιος", "high", False),
("Εκπαίδευση νέου σερβιτόρου στο σύστημα", "normal", False),
("Ανανέωση άδειας λειτουργίας", "high", False),
("Παραγγελία νέων χαρτοπετσετών και υλικών bar", "normal", True),
("Φωτογράφιση νέων πιάτων για online menu", "normal", False),
]
for body, priority, is_done in todos_data:
todo = SiteTodo(
body=body,
priority=priority,
is_done=is_done,
created_by_id=manager.id,
done_by_id=manager.id if is_done else None,
done_at=_utc(datetime.now(timezone.utc) - timedelta(days=2)) if is_done else None,
)
db.add(todo)
db.commit()
print(" notes & todos done")
# ── Schedule (next 3 weeks) ───────────────────────────────────────────────
print("Adding schedule for next 3 weeks...")
# Each waiter works 5 days out of 7, with realistic shift patterns
# Mon-Fri: full shifts, Sat: both sessions, Sun: afternoon/evening only
shift_patterns = {
0: [("10:00", "16:00"), ("16:00", "23:30")], # Mon — two shifts
1: [("10:00", "16:00"), ("16:00", "23:30")], # Tue
2: [("10:00", "16:00"), ("16:00", "23:30")], # Wed
3: [("10:00", "16:00"), ("16:00", "23:30")], # Thu
4: [("10:00", "17:00"), ("17:00", "23:30")], # Fri
5: [("10:00", "16:00"), ("16:00", "23:30")], # Sat — busy day
6: [("11:00", "16:00"), ("16:00", "23:00")], # Sun
}
# Assign waiters to shifts: each day, 2 waiters per slot
for weeks_ahead in range(3):
for day_offset in range(7):
sched_date = today + timedelta(weeks=weeks_ahead, days=day_offset)
dow = sched_date.weekday()
day_patterns = shift_patterns[dow]
# Shuffle waiters and assign 2 per slot
waiter_pool = waiters[:]
random.shuffle(waiter_pool)
for slot_idx, (start_t, end_t) in enumerate(day_patterns):
# Pick 2 waiters per slot (rotate through pool)
slot_waiters = [waiter_pool[slot_idx % len(waiter_pool)],
waiter_pool[(slot_idx + 1) % len(waiter_pool)]]
for w in slot_waiters:
# Don't double-schedule same waiter on same day
already = db.query(ScheduledShift).filter(
ScheduledShift.user_id == w.id,
ScheduledShift.scheduled_date == sched_date,
).first()
if not already:
sched = ScheduledShift(
user_id=w.id,
scheduled_date=sched_date,
start_time=start_t,
end_time=end_t,
notes=None,
created_by_id=manager.id,
)
db.add(sched)
db.commit()
print(" schedule done")
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} | nickname: {w.nickname}")
finally:
db.close()