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>
This commit is contained in:
2026-07-19 10:00:14 +03:00
parent 02ec1aa28f
commit 34ae328b0d
182 changed files with 34874 additions and 3556 deletions

View File

@@ -4,22 +4,28 @@ 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)
- 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] marker before inserting anything.
DEMO guard before inserting anything.
"""
import json
import random
import sys
from datetime import datetime, timedelta, timezone
from datetime import datetime, timedelta, date, timezone
import bcrypt
from sqlalchemy import text
@@ -30,12 +36,19 @@ 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
@@ -43,6 +56,11 @@ 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)
@@ -83,34 +101,58 @@ def _audit(db, order_id, event_type, waiter_id=None, item_ids=None,
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), ...])
# (category_name, color, [(product_name, price, cost), ...])
MENU = [
("Ορεκτικά", "#f97316", [
("Τζατζίκι", 4.50),
("Ταραμοσαλάτα", 4.50),
("Χωριάτικη Σαλάτα", 7.50),
("Κεφτεδάκια", 8.00),
("Σαγανάκι", 6.50),
("Χούμους", 5.00),
("Τζατζίκι", 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),
("Μπριζόλα Μοσχαρίσια", 16.00),
("Σουβλάκι Κοτόπουλο", 10.00),
("Μουσακάς", 11.00),
("Παστίτσιο", 10.00),
("Σπαγγέτι Μπολονέζ", 9.50),
("Γύρος Χοιρινός", 8.50),
("Μπριζόλα Χοιρινή", 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),
("Αναψυκτικό", 2.50),
("Μπύρα Φιάλη", 4.00),
("Κρασί (ποτήρι)", 4.50),
("Καφές Ελληνικός", 2.50),
("Φραπέ", 3.00),
("Χυμός Πορτοκάλι", 3.50),
("Νερό 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),
]),
]
@@ -118,23 +160,30 @@ MENU = [
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.full_name.like("[DEMO]%")).first()
existing = db.query(User).filter(User.username == "demo_nikos").first()
if existing:
print("Demo data already present (found [DEMO] user). Nothing to do.")
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)
@@ -155,7 +204,6 @@ try:
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()
@@ -163,22 +211,21 @@ try:
# ── Waiters ───────────────────────────────────────────────────────────────
waiters = []
for nickname, full_name in WAITER_NAMES:
username = f"demo_{nickname.lower()}"
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(str(random.randint(1000, 9999))),
pin_hash=_hash_pin(pin),
role="waiter",
full_name=f"[DEMO] {full_name}",
full_name=full_name,
nickname=nickname,
is_active=True,
)
db.add(w)
db.flush()
waiters.append(w)
print(f" created {len(waiters)} demo waiters")
print(f" created {len(waiters)} waiters")
# ── Table groups & tables ─────────────────────────────────────────────────
group_configs = [
@@ -201,7 +248,7 @@ try:
all_tables.append(tbl)
print(f" created {len(all_tables)} tables across {len(group_configs)} groups")
# ── Waiter zone assignments (all waiters → all zones) ─────────────────────
# ── Waiter zone assignments ───────────────────────────────────────────────
for w in waiters:
existing_zone = db.query(WaiterZone).filter(
WaiterZone.waiter_id == w.id, WaiterZone.group_id.is_(None)
@@ -212,8 +259,8 @@ try:
# ── Menu ──────────────────────────────────────────────────────────────────
categories = []
all_products = [] # flat list for random picks
category_product_map = [] # parallel list of product sub-lists (for weighted pick)
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()
@@ -224,24 +271,82 @@ try:
categories.append(cat)
cat_products = []
for prod_sort, (prod_name, prod_price) in enumerate(items):
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")
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()
@@ -252,10 +357,14 @@ try:
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]
# 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,
@@ -270,12 +379,13 @@ try:
)
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 = {} # shift_id → running revenue total, filled during order loop
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))
@@ -291,51 +401,53 @@ try:
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
# 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(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))
for _ in range(n_pos):
order_times.append(_random_time_in_day(day_date, HOUR_WEIGHTS))
order_times.sort()
# Track which tables are "busy" at each point in time so we don't double-seat
# Format: {table_id: release_time}
# 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)
# 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)
free_tables = all_tables
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
# ~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,
@@ -344,21 +456,16 @@ try:
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,
))
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 = []
@@ -368,10 +475,11 @@ try:
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)
# ~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
order_total += line_total
item = OrderItem(
order_id=order.id,
@@ -379,44 +487,384 @@ try:
added_by=waiter.id,
quantity=qty,
unit_price=product.base_price,
status="paid",
unit_cost=product.cost_simple,
status="cancelled" if is_cancelled else "paid",
printed=True,
paid_by=waiter.id,
paid_at=_utc(paid_at),
payment_method=payment_method,
paid_in_shift_id=shift.id,
)
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)
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)
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)
_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
# ── 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")
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}")
@@ -424,7 +872,7 @@ try:
print(f"\nLogin credentials:")
print(f" Manager — username: manager | password: password | PIN: 1234")
for w in waiters:
print(f" Waiter — username: {w.username}")
print(f" Waiter — username: {w.username} | nickname: {w.nickname}")
finally:
db.close()