Spaces:
Sleeping
Sleeping
File size: 24,058 Bytes
95ce940 8cc1dc6 bb1b24a 8cc1dc6 95ce940 c72a5e1 8cc1dc6 d01b5af bb1b24a 95ce940 48277b2 95ce940 48277b2 aa31af3 1a64ded 67240b6 1a64ded aa31af3 95ce940 67240b6 aa31af3 67240b6 95ce940 67240b6 95ce940 d01b5af aa31af3 d01b5af aa31af3 d01b5af 48277b2 95ce940 48277b2 aa31af3 48277b2 1a64ded ec20469 3fd6de1 1a64ded 67240b6 ec20469 c72a5e1 32e0adb ec20469 32e0adb ec20469 32e0adb ec20469 48277b2 67240b6 95ce940 1a64ded aa31af3 1a64ded bb1b24a ec20469 aa31af3 ec20469 1a64ded 95ce940 1a64ded ec20469 bb1b24a ec20469 1a64ded ec20469 bb1b24a 67240b6 1a64ded ec20469 1a64ded 67240b6 95ce940 67240b6 1a64ded 95ce940 67240b6 1a64ded d01b5af 67240b6 d01b5af 67240b6 1a64ded 67240b6 ec20469 1a64ded ec20469 1a64ded ec20469 1a64ded 67240b6 8cc1dc6 bb1b24a ec20469 bb1b24a 67240b6 ec20469 1a64ded ec20469 bb1b24a ec20469 1a64ded ec20469 1a64ded bb1b24a 1a64ded 67240b6 ec20469 67240b6 95ce940 1a64ded 95ce940 1a64ded aa31af3 1a64ded ec20469 1a64ded 67240b6 48277b2 ec20469 bb1b24a 1a64ded d01b5af 1a64ded d01b5af 1a64ded ec20469 aa31af3 95ce940 67240b6 95ce940 ec20469 48277b2 bb1b24a ec20469 95ce940 48277b2 ec20469 67240b6 ec20469 48277b2 ec20469 d01b5af ec20469 95ce940 48277b2 1a64ded 95ce940 1a64ded 95ce940 bb1b24a 1a64ded bb1b24a d01b5af 1a64ded d01b5af 95ce940 48277b2 95ce940 1a64ded ec20469 bb1b24a 67240b6 aa31af3 ec20469 aa31af3 ec20469 aa31af3 ec20469 95ce940 67240b6 d01b5af 67240b6 ec20469 bb1b24a 8cc1dc6 95ce940 ec20469 1a64ded | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 | import gradio as gr
import sqlite3
import datetime
import os
import uuid
import pandas as pd # For Excel export
from PIL import Image, ImageDraw, ImageFont # For JPG receipt
import tempfile
DB_FILE = "pos_app.db"
FONT_FILE = "DejaVuSansMono.ttf"
RECEIPT_WIDTH = 300
RECEIPT_LINE_HEIGHT = 15
RECEIPT_MARGIN = 10
# --- Database Functions ---
def init_db():
"""Initialize the database and tables if they don't exist."""
conn = sqlite3.connect(DB_FILE); cursor = conn.cursor()
cursor.execute("CREATE TABLE IF NOT EXISTS products (id TEXT PRIMARY KEY, name TEXT NOT NULL, cost_price REAL NOT NULL DEFAULT 0, price REAL NOT NULL, stock INTEGER NOT NULL)")
cursor.execute("CREATE TABLE IF NOT EXISTS transactions (id TEXT PRIMARY KEY, timestamp TEXT NOT NULL, total REAL NOT NULL, payment_method TEXT, amount_paid REAL NOT NULL, change REAL NOT NULL)")
cursor.execute("CREATE TABLE IF NOT EXISTS transaction_items (id INTEGER PRIMARY KEY AUTOINCREMENT, transaction_id TEXT NOT NULL, product_id TEXT NOT NULL, product_name TEXT NOT NULL, cost_at_transaction REAL NOT NULL DEFAULT 0, price_at_transaction REAL NOT NULL, quantity INTEGER NOT NULL, subtotal REAL NOT NULL, FOREIGN KEY (transaction_id) REFERENCES transactions(id), FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE RESTRICT)")
cursor.execute("CREATE TABLE IF NOT EXISTS app_state (key TEXT PRIMARY KEY, value REAL NOT NULL)")
cursor.execute("INSERT OR IGNORE INTO app_state (key, value) VALUES (?, ?)", ('current_cash_balance', 0.0))
cursor.execute("INSERT OR IGNORE INTO app_state (key, value) VALUES (?, ?)", ('starting_cash_balance', 0.0))
cursor.execute("INSERT OR IGNORE INTO app_state (key, value) VALUES (?, ?)", ('qris_total_received', 0.0))
cursor.execute("INSERT OR IGNORE INTO app_state (key, value) VALUES (?, ?)", ('card_total_received', 0.0))
conn.commit(); conn.close()
def db_execute(query, params=()):
conn = sqlite3.connect(DB_FILE); cursor = conn.cursor()
try: cursor.execute(query, params); conn.commit(); return cursor.rowcount if query.strip().upper().startswith(("DELETE", "UPDATE")) else cursor.lastrowid
except sqlite3.Error as e: print(f"DB Execute Error: {e}"); conn.rollback(); return None
finally: conn.close()
def db_query(query, params=()):
conn = sqlite3.connect(DB_FILE); conn.row_factory = sqlite3.Row; cursor = conn.cursor()
try: cursor.execute(query, params); rows = cursor.fetchall(); return [dict(row) for row in rows]
except sqlite3.Error as e: print(f"DB Query Error: {e}"); return []
finally: conn.close()
def get_app_state(key, default_value=0.0):
result = db_query("SELECT value FROM app_state WHERE key = ?", (key,)); return result[0]['value'] if result else default_value
def update_app_state(key, value): db_execute("INSERT OR REPLACE INTO app_state (key, value) VALUES (?, ?)", (key, value)); print(f"App State Updated: {key} = {value}")
# --- Business Logic Functions ---
def get_products_for_display_lol():
products = db_query("SELECT id, name, cost_price, price, stock FROM products ORDER BY name"); return [] if not products else [[p.get(k,0.0 if k in ('cost_price','price') else (0 if k=='stock' else '')) for k in ['id','name','cost_price','price','stock']] for p in products]
def _validate_product_input(pid, n, cp_s, p_s, st_s, is_new=True):
if not all([pid, n, cp_s, p_s, st_s]): return None, None, None, None, None, "Err: All fields req."
try:
cp=float(cp_s); ps=float(p_s); si=int(st_s)
if cp < 0 or ps <= 0 or si < 0: return None,None,None,None,None, "Err: Invalid numbers."
if is_new and db_query("SELECT id FROM products WHERE id = ?", (pid,)): return None,None,None,None,None, f"Err: ID '{pid}' exists."
return pid, n, cp, ps, si, None # Valid
except: return None, None, None, None, None, "Err: Cost/Price/Stock numbers invalid."
def add_product(pid, n, cp_s, p_s, st_s):
pid, n, cp, ps, si, err = _validate_product_input(pid, n, cp_s, p_s, st_s, is_new=True)
if err: return err, gr.update(value=get_products_for_display_lol())
res = db_execute("INSERT INTO products VALUES (?,?,?,?,?)", (pid, n, cp, ps, si))
msg = f"OK: '{n}' added." if res is not None else "Err: Failed add."
return msg, gr.update(value=get_products_for_display_lol())
def update_product(pid, n, cp_s, p_s, st_s):
pid, n, cp, ps, si, err = _validate_product_input(pid, n, cp_s, p_s, st_s, is_new=False)
if err: return err, gr.update(value=get_products_for_display_lol())
res = db_execute("UPDATE products SET name=?, cost_price=?, price=?, stock=? WHERE id=?", (n, cp, ps, si, pid))
msg = f"OK: '{pid}' updated." if res > 0 else (f"Warn: ID '{pid}' not found." if res==0 else "Err: Update failed.")
return msg, gr.update(value=get_products_for_display_lol())
def delete_product(pid):
if not pid: return "Err: ID required.", gr.update(value=get_products_for_display_lol())
msg = ""; q = "DELETE FROM products WHERE id = ?"
try: res = db_execute(q, (pid,)); msg = f"OK: '{pid}' deleted." if res > 0 else (f"Err: ID '{pid}' not found." if res==0 else "Err: DB error.")
except sqlite3.IntegrityError: msg = f"Err: Cannot delete '{pid}', has history."
except Exception as e: msg = f"Err: {e}"
return msg, gr.update(value=get_products_for_display_lol())
def load_product_for_edit(pid):
if not pid: return "Enter ID to load.", "", "", "", "", ""
p = get_product_details(pid)
if p: return (f"Loaded '{p.get('name','')}'.", gr.update(value=p.get('id','')), gr.update(value=p.get('name','')), gr.update(value=p.get('cost_price',0.0)), gr.update(value=p.get('price',0.0)), gr.update(value=p.get('stock',0)))
else: return f"Err: ID '{pid}' not found.", gr.update(value=pid), *[gr.update(value="")]*4
# **FIXED SyntaxError here**
def get_product_choices_initial():
"""Gets product choices as list[tuple] for initial dropdown load."""
products = db_query("SELECT id, name, price, stock FROM products WHERE stock > 0 ORDER BY name")
choices = []
if products:
try: # Correct indentation
choices = [(f"{p.get('name', 'N/A')} (Stock: {p.get('stock', 0)}, Price: {p.get('price', 0):.2f})", p.get('id'))
for p in products if isinstance(p, dict) and p.get('id')]
except Exception as e: # Correct indentation
print(f"Error formatting initial choices: {e}")
choices = []
return choices
def get_product_choices_for_update(): return gr.update(choices=get_product_choices_initial())
def get_product_details(pid): return db_query("SELECT * FROM products WHERE id = ?", (pid,))[0] if isinstance(pid, str) and pid else None
def generate_receipt_image(txt):
try:
if not os.path.exists(FONT_FILE): print(f"Font Error:'{FONT_FILE}' missing."); return None
font=ImageFont.truetype(FONT_FILE, size=12)
except IOError: print(f"Font Error:Cannot load '{FONT_FILE}'."); return None
lines=txt.strip().split('\n'); cw=7; maxl=max(len(l) for l in lines if l);
iw=max(maxl*cw+2*RECEIPT_MARGIN, RECEIPT_WIDTH); ih=(len(lines)*RECEIPT_LINE_HEIGHT)+2*RECEIPT_MARGIN
img=Image.new('RGB',(int(iw),int(ih)),color='white'); draw=ImageDraw.Draw(img); y=RECEIPT_MARGIN
for l in lines: draw.text((RECEIPT_MARGIN,y),l,fill='black',font=font); y+=RECEIPT_LINE_HEIGHT
try:
with tempfile.NamedTemporaryFile(delete=False, suffix=".jpg", prefix="rcpt_") as f: img.save(f,format="JPEG"); return f.name
except Exception as e: print(f"Image Save Error:{e}"); return None
def process_sale(cart_items, payment_method, amount_paid_str):
current_cash = get_app_state('current_cash_balance'); current_qris = get_app_state('qris_total_received'); current_card = get_app_state('card_total_received')
fmt_curr_cash=format_balance(current_cash); fmt_qris=format_balance(current_qris); fmt_card=format_balance(current_card) # Format current state for potential early return
if not cart_items: return "Cart empty.", "", None, fmt_curr_cash, fmt_qris, fmt_card
if not payment_method: return "Select payment method.", "", None, fmt_curr_cash, fmt_qris, fmt_card
total_amount = sum(item['subtotal'] for item in cart_items); amount_paid=0.0; change=0.0; new_cash=current_cash; new_qris=current_qris; new_card=current_card
if payment_method == 'Cash':
try: amount_paid = float(amount_paid_str) if amount_paid_str else 0.0; assert amount_paid >= 0
except: return "Invalid cash amount.", "", None, fmt_curr_cash, fmt_qris, fmt_card
if amount_paid < total_amount: return "Amount insufficient.", "", None, fmt_curr_cash, fmt_qris, fmt_card
change = amount_paid - total_amount; new_cash = current_cash + amount_paid - change
elif payment_method == 'QRIS': amount_paid = total_amount; change = 0.0; new_qris = current_qris + total_amount
elif payment_method == 'Card': amount_paid = total_amount; change = 0.0; new_card = current_card + total_amount
else: return "Invalid payment method.", "", None, fmt_curr_cash, fmt_qris, fmt_card
trx_id = f"TRX-{uuid.uuid4().hex[:8].upper()}"; ts = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
conn = sqlite3.connect(DB_FILE); cursor = conn.cursor()
try:
cursor.execute("BEGIN TRANSACTION"); cursor.execute("INSERT INTO transactions VALUES (?,?,?,?,?,?)", (trx_id, ts, total_amount, payment_method, amount_paid, change))
items_data=[]; stock_updates=[]
for item in cart_items:
cost = item.get('cost_price', 0.0); items_data.append((trx_id, item['id'], item['nama'], cost, item['harga'], item['jumlah'], item['subtotal']))
stock_row=cursor.execute("SELECT stock FROM products WHERE id = ?", (item['id'],)).fetchone(); cur_stock=stock_row[0] if stock_row else 0
if cur_stock < item['jumlah']: raise ValueError(f"Stock error: {item.get('nama','Item')}")
stock_updates.append((item['jumlah'], item['id']))
cursor.executemany("INSERT INTO transaction_items VALUES (NULL,?,?,?,?,?,?,?)", items_data)
cursor.executemany("UPDATE products SET stock = stock - ? WHERE id = ?", stock_updates)
if payment_method == 'Cash': update_app_state('current_cash_balance', new_cash)
elif payment_method == 'QRIS': update_app_state('qris_total_received', new_qris)
elif payment_method == 'Card': update_app_state('card_total_received', new_card)
conn.commit()
receipt = f"-- Receipt --\nID: {trx_id}\nT: {ts}\nM: {payment_method}\n------------------------\n{'Name':<15} {'Qty':<3} {'Price':<8} {'Subtotal':<9}\n";
for i in cart_items: receipt += f"{i.get('nama','N/A')[:15]:<15} {i.get('jumlah',0):<3} {i.get('harga',0.0):<8.2f} {i.get('subtotal',0.0):<9.2f}\n"
receipt += f"------------------------\nTotal : {total_amount:>10,.2f}\n";
if payment_method=='Cash': receipt += f"Paid : {amount_paid:>10,.2f}\nChange: {change:>10,.2f}\n"
receipt += "------------------------\n Thank You! \n"; img_path = generate_receipt_image(receipt)
status = f"OK! Change: {change:,.2f}" if payment_method=='Cash' else "OK!";
return status, receipt, img_path, format_balance(new_cash), format_balance(new_qris), format_balance(new_card)
except Exception as e: conn.rollback(); print(f"Sale Error: {e}"); return f"Error: {e}", "", None, fmt_curr_cash, fmt_qris, fmt_card
finally: conn.close()
def generate_excel_report():
"""Generates an Excel report with sales, items, stock, and financial summary."""
try:
conn=sqlite3.connect(DB_FILE)
sales = pd.read_sql_query("SELECT * FROM transactions ORDER BY timestamp DESC", conn)
items = pd.read_sql_query("SELECT t.timestamp, ti.*, (ti.price_at_transaction - ti.cost_at_transaction) * ti.quantity as profit FROM transaction_items ti JOIN transactions t ON ti.transaction_id = t.id ORDER BY t.timestamp DESC", conn)
stock = pd.read_sql_query("SELECT id, name, cost_price, price, stock FROM products ORDER BY name", conn)
start_cash=get_app_state('starting_cash_balance'); current_cash=get_app_state('current_cash_balance')
qris_total=get_app_state('qris_total_received'); card_total=get_app_state('card_total_received')
conn.close()
total_revenue=sales['total'].sum() if not sales.empty else 0; total_cogs=items['cost_at_transaction'].multiply(items['quantity']).sum() if not items.empty else 0
gross_profit=items['profit'].sum() if not items.empty else 0; net_cash_change=current_cash-start_cash; num_trx=len(sales)
summary_data = {'Metric': ['Start Cash','Current Cash','Net Cash Chg','Total QRIS Rcvd','Total Card Rcvd','Total Revenue','Total COGS','Gross Profit','Num Transactions'],
'Amount': [f"{start_cash:,.2f}",f"{current_cash:,.2f}",f"{net_cash_change:,.2f}",f"{qris_total:,.2f}",f"{card_total:,.2f}",f"{total_revenue:,.2f}",f"{total_cogs:,.2f}",f"{gross_profit:,.2f}",num_trx]}
summary_df = pd.DataFrame(summary_data)
with tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx", prefix="rpt_") as f: excel_path = f.name
with pd.ExcelWriter(excel_path, engine='openpyxl') as writer:
summary_df.to_excel(writer, sheet_name='Financial Summary', index=False)
sales.to_excel(writer, sheet_name='Sales Summary', index=False)
items.to_excel(writer, sheet_name='Item Details', index=False)
stock.to_excel(writer, sheet_name='Stock Levels', index=False)
for sheet_name in writer.sheets: # Auto-adjust columns
worksheet = writer.sheets[sheet_name]
for col in worksheet.columns:
# --- FIXED SYNTAX ---
try: # Start try on new line
max_length = 0; column_letter = col[0].column_letter
for cell in col:
if cell.value is not None: max_length = max(max_length, len(str(cell.value)))
adjusted_width = max_length + 2
worksheet.column_dimensions[column_letter].width = adjusted_width
except Exception as e: # Catch errors for specific columns
# print(f"ColWidth Err: {e}") # Optional debug
pass
print(f"Excel report generated: {excel_path}"); return excel_path
except Exception as e: print(f"Excel Error: {e}"); gr.Error(f"Report Error: {e}"); return None
def add_item_to_cart(pid, qty, cart):
if not isinstance(pid, str) or qty <= 0: gr.Warning("Invalid input."); return cart
p=get_product_details(pid);
if not p: gr.Warning(f"ID '{pid}' not found."); return cart
if qty > p['stock']: gr.Warning(f"Stock low: {p['name']} ({p['stock']})."); return cart
found=False
for item in cart:
if item['id'] == p['id']:
nq=item['jumlah']+qty;
if nq > p['stock']: gr.Warning(f"Cart qty ({nq}) exceeds stock: {p['name']} ({p['stock']})."); return cart
item['jumlah']=nq; item['subtotal']=item['harga']*nq; found=True; gr.Info(f"Qty updated."); break
if not found: cart.append({"id":p['id'], "nama":p['name'], "cost_price":p['cost_price'], "harga":p['price'], "jumlah":qty, "subtotal":p['price']*qty }); gr.Info(f"Item added.")
return cart
def display_cart_and_total(cart_state):
total=0.0; data_lol=[]
if cart_state:
for item in cart_state: data_lol.append([item.get(k,'N/A' if k=='nama' else 0) for k in ['nama','jumlah','harga','subtotal']]); total += item.get('subtotal', 0.0)
# Return gr.update for the DataFrame value and the formatted total string
return gr.update(value=data_lol), f"Total: {total:,.2f}"
def format_balance(balance=None, key=None):
bal_value = 0.0
if balance is not None: bal_value = balance
elif key is not None: bal_value = get_app_state(key)
try: return f"Rp {float(bal_value):,.2f}".replace(",", "X").replace(".", ",").replace("X", ".")
except: return "Rp Error"
def reset_cashier_ui():
return [], 1, "Total: 0.00", None, "", get_product_choices_for_update() # cart, qty, total, payment_method, amount_paid, product_dropdown
def handle_report_generation(): excel_path=generate_excel_report(); return gr.update(value=excel_path, visible=bool(excel_path))
def handle_set_starting_cash(new_start_cash_str):
try:
new_start = float(new_start_cash_str); assert new_start >= 0
update_app_state('starting_cash_balance', new_start)
update_app_state('current_cash_balance', new_start)
update_app_state('qris_total_received', 0.0); update_app_state('card_total_received', 0.0)
gr.Info("Starting cash and totals reset!");
return format_balance(new_start), format_balance(new_start), format_balance(0.0), format_balance(0.0) # Update all 4 displays
except: gr.Warning("Invalid starting cash."); start_b=get_app_state('starting_cash_balance'); curr_b=get_app_state('current_cash_balance'); qris_b=get_app_state('qris_total_received'); card_b=get_app_state('card_total_received'); return format_balance(start_b), format_balance(curr_b), format_balance(qris_b), format_balance(card_b)
init_db()
with gr.Blocks(title="POS App v1.6 (CRUD)", theme=gr.themes.Default()) as demo:
gr.Markdown("# Simple Point of Sale Application")
cart_state = gr.State([])
init_start_cash=get_app_state('starting_cash_balance'); init_curr_cash=get_app_state('current_cash_balance'); init_qris=get_app_state('qris_total_received'); init_card=get_app_state('card_total_received')
with gr.Tabs():
with gr.TabItem("π POS / Cashier"):
with gr.Row():
starting_cash_display=gr.Textbox(label="Starting Cash", value=format_balance(init_start_cash), interactive=False)
current_cash_display=gr.Textbox(label="Current Cash", value=format_balance(init_curr_cash), interactive=False)
qris_balance_display=gr.Textbox(label="QRIS Total", value=format_balance(init_qris), interactive=False)
card_balance_display=gr.Textbox(label="Card Total", value=format_balance(init_card), interactive=False)
gr.Markdown("---")
with gr.Row():
with gr.Column(scale=1):
product_dropdown=gr.Dropdown(label="Select Product", choices=get_product_choices_initial(), interactive=True)
quantity_input=gr.Number(label="Quantity", value=1, minimum=1, step=1, interactive=True)
add_to_cart_btn=gr.Button("β Add to Cart", variant="secondary")
payment_method_input=gr.Radio(label="Payment Method", choices=["Cash", "QRIS", "Card"], value=None, interactive=True)
payment_input=gr.Textbox(label="Amount Paid (Cash Only)", interactive=True, type="text")
process_payment_btn=gr.Button("π° Process Payment", variant="primary")
with gr.Column(scale=2):
gr.Markdown("#### Shopping Cart")
cart_display=gr.DataFrame(headers=["Name", "Qty", "Price", "Subtotal"], value=[], interactive=False, datatype=["str", "number", "number", "number"])
total_display=gr.Textbox("Total: 0.00", label="Total Amount", interactive=False)
gr.Markdown("---")
status_transaksi=gr.Textbox(label="Status", interactive=False)
receipt_display=gr.Textbox(label="Receipt (Text)", lines=8, interactive=False)
receipt_jpg_file=gr.File(label="Download Receipt (JPG)", visible=False, interactive=False)
with gr.TabItem("π¦ Product Management"):
gr.Markdown("Use Product ID + Load/Delete. Fill fields + Save New/Update.")
with gr.Row(): product_id_input = gr.Textbox(label="Product ID"); load_product_btn = gr.Button("π Load")
with gr.Row(): product_name_input = gr.Textbox(label="Name"); product_cost_price_input = gr.Textbox(label="Cost Price")
with gr.Row(): product_price_input = gr.Textbox(label="Selling Price"); product_stock_input = gr.Textbox(label="Stock")
with gr.Row(): save_new_product_btn=gr.Button("πΎ Save New"); update_product_btn=gr.Button("π Update",variant="primary"); delete_product_btn=gr.Button("β Delete",variant="stop")
status_produk = gr.Textbox(label="Status", interactive=False)
gr.Markdown("---"); gr.Markdown("#### Products List")
product_display = gr.DataFrame(headers=["ID", "Name", "Cost", "Price", "Stock"], value=get_products_for_display_lol(), interactive=False, datatype=["str", "str", "number", "number", "number"])
with gr.TabItem("π Reports"):
gr.Markdown("Generate financial & stock reports."); generate_report_btn = gr.Button("Generate Excel Report"); excel_report_file = gr.File(label="Download Report (Excel)", visible=False, interactive=False)
with gr.TabItem("βοΈ Settings"):
gr.Markdown("Set starting cash (resets current cash, QRIS, Card totals)."); set_starting_cash_input = gr.Textbox(label="Set Starting Cash (Modal Awal)", placeholder="Enter amount", type="text"); set_starting_cash_btn = gr.Button("Set Starting Cash / Reset Day")
# --- Connect UI Actions to Functions ---
# POS Tab Actions
add_to_cart_btn.click(fn=add_item_to_cart, inputs=[product_dropdown, quantity_input, cart_state], outputs=[cart_state])\
.then(fn=display_cart_and_total, inputs=[cart_state], outputs=[cart_display, total_display])
process_payment_btn.click(fn=process_sale, inputs=[cart_state, payment_method_input, payment_input], outputs=[status_transaksi, receipt_display, receipt_jpg_file, current_cash_display, qris_balance_display, card_balance_display])\
.then(lambda img_path: gr.update(visible=bool(img_path)), inputs=[receipt_jpg_file], outputs=[receipt_jpg_file])\
.then(fn=reset_cashier_ui, inputs=[], outputs=[cart_state, quantity_input, total_display, payment_method_input, payment_input, product_dropdown])\
.then(fn=display_cart_and_total, inputs=[cart_state], outputs=[cart_display, total_display]) # Clear visual cart
# Product Management Tab Actions
load_product_btn.click(fn=load_product_for_edit, inputs=[product_id_input], outputs=[status_produk, product_id_input, product_name_input, product_cost_price_input, product_price_input, product_stock_input])
save_new_product_btn.click(fn=add_product, inputs=[product_id_input, product_name_input, product_cost_price_input, product_price_input, product_stock_input], outputs=[status_produk, product_display])\
.then(fn=get_product_choices_for_update, inputs=[], outputs=[product_dropdown]) # Refresh dropdown
update_product_btn.click(fn=update_product, inputs=[product_id_input, product_name_input, product_cost_price_input, product_price_input, product_stock_input], outputs=[status_produk, product_display])\
.then(fn=get_product_choices_for_update, inputs=[], outputs=[product_dropdown]) # Refresh dropdown
delete_product_btn.click(fn=delete_product, inputs=[product_id_input], outputs=[status_produk, product_display])\
.then(fn=get_product_choices_for_update, inputs=[], outputs=[product_dropdown]) # Refresh dropdown
# Reporting Tab Actions
generate_report_btn.click(fn=handle_report_generation, inputs=[], outputs=[excel_report_file])
# Settings Tab Actions
set_starting_cash_btn.click(fn=handle_set_starting_cash, inputs=[set_starting_cash_input], outputs=[starting_cash_display, current_cash_display, qris_balance_display, card_balance_display])
if __name__ == "__main__":
init_db(); print(f"DB:'{DB_FILE}'. Check permissions.");
if not os.path.exists(FONT_FILE): print(f"WARN: Font '{FONT_FILE}' missing. JPG receipt disabled.")
print("Launching Gradio..."); demo.launch() |