Pembayaran

This commit is contained in:
Jevinca Marvella 2025-12-13 15:16:01 +07:00
parent 067e6b2e17
commit 123269fc23
5 changed files with 516 additions and 6 deletions

View File

@ -1,2 +1,2 @@
id,transaksi_id,menu_id,qty,harga_satuan,subtotal_item
1,1,1,1,20000.0,20000.0

1 id transaksi_id menu_id qty harga_satuan subtotal_item
2 1 1 1 1 20000.0 20000.0

View File

@ -1 +1,2 @@
user_id,menu_id,order_count,last_ordered
user_id,menu_id,order_count,last_ordered
4,1,1,2025-12-13 15:09:50

1 user_id menu_id order_count last_ordered
2 4 1 1 2025-12-13 15:09:50

513
main.py
View File

@ -130,6 +130,7 @@ def seed_defaults():
})
write_all(PROMO_CSV, ["code","type","value","min_total"], rows)
# Seed meja (10 meja default)
meja_rows = read_all(MEJA_CSV)
if not meja_rows:
@ -736,6 +737,11 @@ class App:
# Tab khusus waiter
if self.session['role'] in ['waiter', 'admin']:
main.add(self.tab_waiter, text="Waiter - Pesanan")
# Tab pembayaran untuk kasir & admin
if self.session['role'] in ['kasir', 'admin']:
self.tab_payment = ttk.Frame(main)
main.add(self.tab_payment, text="💰 Pembayaran")
# Tab khusus admin
if self.session['role'] == 'admin':
@ -754,6 +760,9 @@ class App:
if self.session['role'] in ['waiter', 'admin']:
self.build_waiter_tab(self.tab_waiter)
if self.session['role'] in ['kasir', 'admin']:
self.build_payment_tab(self.tab_payment)
if self.session['role'] == 'admin':
self.build_menu_manage_tab(self.tab_menu_manage)
self.build_promo_tab(self.tab_promo)
@ -2110,13 +2119,513 @@ def favorite_all(limit=10):
return out
# FITUR PEMBAYARAN
def build_payment_tab(self, parent):
"""Tab pembayaran untuk kasir & admin"""
for w in parent.winfo_children():
w.destroy()
# Header
header = ttk.Frame(parent)
header.pack(fill='x', padx=10, pady=8)
ttk.Label(header, text="💰 Pembayaran Transaksi", font=("Arial", 14, "bold")).pack(side='left')
ttk.Button(header, text="🔄 Refresh", command=self.reload_payment_orders).pack(side='right', padx=6)
# Split 2 panel: kiri = daftar transaksi, kanan = form pembayaran
left = ttk.LabelFrame(parent, text="📋 Transaksi Siap Dibayar (Status: Selesai)", padding=10)
left.pack(side='left', fill='both', expand=True, padx=10, pady=6)
right = ttk.LabelFrame(parent, text="💳 Form Pembayaran", padding=10)
right.pack(side='right', fill='both', expand=True, padx=10, pady=6)
# === PANEL KIRI: Daftar Transaksi Selesai ===
# Treeview transaksi
cols = ("ID", "Meja", "Total", "Status", "Tanggal")
self.payment_tree = ttk.Treeview(left, columns=cols, show='headings', height=12)
self.payment_tree.heading("ID", text="ID")
self.payment_tree.heading("Meja", text="Meja")
self.payment_tree.heading("Total", text="Total")
self.payment_tree.heading("Status", text="Status")
self.payment_tree.heading("Tanggal", text="Tanggal")
self.payment_tree.column("ID", width=50)
self.payment_tree.column("Meja", width=60)
self.payment_tree.column("Total", width=100)
self.payment_tree.column("Status", width=80)
self.payment_tree.column("Tanggal", width=140)
self.payment_tree.pack(fill='both', expand=True, pady=6)
# Bind event
self.payment_tree.bind("<<TreeviewSelect>>", self.on_payment_select)
# Detail transaksi
detail_frame = ttk.Frame(left)
detail_frame.pack(fill='x', pady=6)
self.payment_detail_text = tk.Text(detail_frame, height=8, font=("Courier New", 8), wrap='word')
detail_scroll = ttk.Scrollbar(detail_frame, orient='vertical', command=self.payment_detail_text.yview)
self.payment_detail_text.configure(yscrollcommand=detail_scroll.set)
self.payment_detail_text.pack(side='left', fill='both', expand=True)
detail_scroll.pack(side='right', fill='y')
# === PANEL KANAN: Form Pembayaran ===
# Info transaksi terpilih
info_frame = ttk.Frame(right)
info_frame.pack(fill='x', pady=10)
self.selected_transaksi_label = ttk.Label(info_frame, text="Belum ada transaksi dipilih", font=("Arial", 10, "bold"), foreground='red')
self.selected_transaksi_label.pack()
self.selected_total_label = ttk.Label(info_frame, text="Total: Rp 0", font=("Arial", 12, "bold"), foreground='green')
self.selected_total_label.pack(pady=4)
ttk.Separator(right, orient='horizontal').pack(fill='x', pady=10)
# Pilih metode pembayaran
method_frame = ttk.Frame(right)
method_frame.pack(fill='x', pady=10)
ttk.Label(method_frame, text="💳 Pilih Metode Pembayaran:", font=("Arial", 10, "bold")).pack(anchor='w', pady=4)
self.payment_method_var = tk.StringVar(value='cash')
ttk.Radiobutton(method_frame, text="💵 Cash", variable=self.payment_method_var, value='cash', command=self.on_payment_method_change).pack(anchor='w', pady=2)
ttk.Radiobutton(method_frame, text="📱 QRIS", variable=self.payment_method_var, value='qris', command=self.on_payment_method_change).pack(anchor='w', pady=2)
ttk.Radiobutton(method_frame, text="💳 E-Wallet (GoPay/OVO/Dana)", variable=self.payment_method_var, value='ewallet', command=self.on_payment_method_change).pack(anchor='w', pady=2)
ttk.Separator(right, orient='horizontal').pack(fill='x', pady=10)
# Frame dinamis untuk input (berganti sesuai metode)
self.payment_input_frame = ttk.Frame(right)
self.payment_input_frame.pack(fill='both', expand=True, pady=10)
# Init cash input (default)
self.build_cash_input()
# Tombol proses pembayaran
ttk.Button(right, text="✅ PROSES PEMBAYARAN", command=self.process_payment, style="Accent.TButton").pack(pady=15, ipadx=20, ipady=10)
# Load data
self.reload_payment_orders()
def reload_payment_orders(self):
"""Load transaksi dengan status 'selesai' yang belum dibayar"""
# Clear tree
for r in self.payment_tree.get_children():
self.payment_tree.delete(r)
# Get transaksi selesai
orders = transaksi_list(status='selesai')
# Filter yang belum ada pembayaran
for order in orders:
tid, uid, meja, total, status, promo_code, tanggal = order
# Cek apakah sudah ada pembayaran
payment_data = pembayaran_get_by_transaksi(tid)
if payment_data:
continue # Skip jika sudah dibayar
self.payment_tree.insert("", tk.END, values=(
tid, meja, f"Rp {total:,.0f}", status, tanggal
))
def on_payment_select(self, event):
"""Tampilkan detail transaksi saat dipilih"""
sel = self.payment_tree.selection()
if not sel:
return
item = self.payment_tree.item(sel)['values']
transaksi_id = item[0]
# Get detail transaksi
transaksi_data = transaksi_get(transaksi_id)
if not transaksi_data:
return
tid, uid, meja, total, status, promo_code, subtotal, item_disc, promo_disc, tanggal = transaksi_data
detail_items = detail_transaksi_list(transaksi_id)
# Update label
self.selected_transaksi_label.config(text=f"Transaksi #{tid} - Meja {meja}", foreground='blue')
self.selected_total_label.config(text=f"Total: Rp {total:,.0f}")
# Format detail
detail_text = f"═══════════════════════════════════════════════\n"
detail_text += f"TRANSAKSI #{tid}\n"
detail_text += f"═══════════════════════════════════════════════\n\n"
detail_text += f"Meja : {meja}\n"
detail_text += f"Tanggal : {tanggal}\n"
detail_text += f"Status : {status.upper()}\n"
detail_text += f"Promo : {promo_code if promo_code else '-'}\n\n"
detail_text += f"───────────────────────────────────────────────\n"
detail_text += f"ITEM PESANAN:\n"
detail_text += f"───────────────────────────────────────────────\n"
for detail in detail_items:
did, mid, qty, harga, subtotal_item = detail
menu_data = menu_get(mid)
if menu_data:
_, nama, kategori, _, _, _, _, _ = menu_data
detail_text += f"{nama}\n"
detail_text += f" {qty} x Rp {harga:,.0f} = Rp {subtotal_item:,.0f}\n\n"
detail_text += f"───────────────────────────────────────────────\n"
detail_text += f"Subtotal : Rp {subtotal:,.0f}\n"
detail_text += f"Diskon : Rp {item_disc + promo_disc:,.0f}\n"
detail_text += f"───────────────────────────────────────────────\n"
detail_text += f"TOTAL BAYAR : Rp {total:,.0f}\n"
detail_text += f"═══════════════════════════════════════════════\n"
self.payment_detail_text.delete('1.0', tk.END)
self.payment_detail_text.insert('1.0', detail_text)
def on_payment_method_change(self):
"""Ganti input form sesuai metode pembayaran"""
method = self.payment_method_var.get()
# Clear frame
for w in self.payment_input_frame.winfo_children():
w.destroy()
if method == 'cash':
self.build_cash_input()
elif method == 'qris':
self.build_qris_input()
elif method == 'ewallet':
self.build_ewallet_input()
def build_cash_input(self):
"""Form input untuk pembayaran cash"""
ttk.Label(self.payment_input_frame, text="💵 Pembayaran Cash", font=("Arial", 10, "bold")).pack(pady=6)
input_frame = ttk.Frame(self.payment_input_frame)
input_frame.pack(fill='x', pady=6)
ttk.Label(input_frame, text="Jumlah Bayar:").grid(row=0, column=0, sticky='w', pady=4)
self.cash_amount_var = tk.StringVar()
ttk.Entry(input_frame, textvariable=self.cash_amount_var, width=20).grid(row=0, column=1, pady=4, sticky='ew')
input_frame.columnconfigure(1, weight=1)
# Label kembalian
self.cash_change_label = ttk.Label(self.payment_input_frame, text="Kembalian: Rp 0", font=("Arial", 10), foreground='green')
self.cash_change_label.pack(pady=6)
# Bind event untuk hitung kembalian real-time
self.cash_amount_var.trace('w', self.calculate_cash_change)
def calculate_cash_change(self, *args):
"""Hitung kembalian cash secara real-time"""
sel = self.payment_tree.selection()
if not sel:
return
item = self.payment_tree.item(sel)['values']
transaksi_id = item[0]
transaksi_data = transaksi_get(transaksi_id)
if not transaksi_data:
return
total = transaksi_data[3]
try:
cash_input = float(self.cash_amount_var.get() or 0)
change = cash_input - total
if change < 0:
self.cash_change_label.config(text=f"Kembalian: Kurang Rp {abs(change):,.0f}", foreground='red')
else:
self.cash_change_label.config(text=f"Kembalian: Rp {change:,.0f}", foreground='green')
except:
self.cash_change_label.config(text="Kembalian: Rp 0", foreground='gray')
def build_qris_input(self):
"""Form input untuk pembayaran QRIS (simulasi QR Code)"""
ttk.Label(self.payment_input_frame, text="📱 Pembayaran QRIS", font=("Arial", 10, "bold")).pack(pady=6)
ttk.Label(self.payment_input_frame, text="Scan QR Code di bawah untuk bayar:", font=("Arial", 9)).pack(pady=4)
# Generate QR Code
sel = self.payment_tree.selection()
if sel:
item = self.payment_tree.item(sel)['values']
transaksi_id = item[0]
transaksi_data = transaksi_get(transaksi_id)
if transaksi_data:
total = transaksi_data[3]
# QR Code data (simulasi)
qr_data = f"QRIS-CAFE-TOTORO-TRX{transaksi_id}-TOTAL{int(total)}"
try:
import qrcode
qr = qrcode.QRCode(version=1, box_size=5, border=2)
qr.add_data(qr_data)
qr.make(fit=True)
qr_img = qr.make_image(fill_color="black", back_color="white")
qr_img = qr_img.resize((200, 200), Image.Resampling.LANCZOS)
qr_photo = ImageTk.PhotoImage(qr_img)
self.img_cache['qris'] = qr_photo
qr_label = ttk.Label(self.payment_input_frame, image=qr_photo)
qr_label.pack(pady=10)
except ImportError:
ttk.Label(self.payment_input_frame, text="[QR Code - Install qrcode library]", background='#e0e0e0', font=("Arial", 9)).pack(pady=10)
ttk.Label(self.payment_input_frame, text="⚠️ Ini adalah SIMULASI QRIS", font=("Arial", 8), foreground='orange').pack(pady=6)
ttk.Label(self.payment_input_frame, text="Klik 'PROSES PEMBAYARAN' untuk konfirmasi", font=("Arial", 8)).pack()
def build_ewallet_input(self):
"""Form input untuk pembayaran E-Wallet (simulasi)"""
ttk.Label(self.payment_input_frame, text="💳 Pembayaran E-Wallet", font=("Arial", 10, "bold")).pack(pady=6)
ttk.Label(self.payment_input_frame, text="Pilih E-Wallet:", font=("Arial", 9)).pack(pady=4)
self.ewallet_type_var = tk.StringVar(value='gopay')
ewallet_frame = ttk.Frame(self.payment_input_frame)
ewallet_frame.pack(pady=6)
ttk.Radiobutton(ewallet_frame, text="GoPay", variable=self.ewallet_type_var, value='gopay').pack(anchor='w', pady=2)
ttk.Radiobutton(ewallet_frame, text="OVO", variable=self.ewallet_type_var, value='ovo').pack(anchor='w', pady=2)
ttk.Radiobutton(ewallet_frame, text="Dana", variable=self.ewallet_type_var, value='dana').pack(anchor='w', pady=2)
ttk.Radiobutton(ewallet_frame, text="ShopeePay", variable=self.ewallet_type_var, value='shopeepay').pack(anchor='w', pady=2)
ttk.Label(self.payment_input_frame, text="⚠️ Ini adalah SIMULASI E-Wallet", font=("Arial", 8), foreground='orange').pack(pady=6)
ttk.Label(self.payment_input_frame, text="Klik 'PROSES PEMBAYARAN' untuk konfirmasi", font=("Arial", 8)).pack()
def process_payment(self):
"""Proses pembayaran sesuai metode yang dipilih"""
# Cek apakah ada transaksi terpilih
sel = self.payment_tree.selection()
if not sel:
messagebox.showwarning("Pilih Transaksi", "Pilih transaksi yang akan dibayar")
return
item = self.payment_tree.item(sel)['values']
transaksi_id = item[0]
transaksi_data = transaksi_get(transaksi_id)
if not transaksi_data:
messagebox.showerror("Error", "Data transaksi tidak ditemukan")
return
tid, uid, meja, total, status, promo_code, subtotal, item_disc, promo_disc, tanggal = transaksi_data
# Get metode pembayaran
method = self.payment_method_var.get()
# Validasi & proses sesuai metode
if method == 'cash':
success, message = self.process_cash_payment(tid, total)
elif method == 'qris':
success, message = self.process_qris_payment(tid, total)
elif method == 'ewallet':
success, message = self.process_ewallet_payment(tid, total)
else:
messagebox.showerror("Error", "Metode pembayaran tidak valid")
return
if success:
# Generate struk
struk = self.generate_struk(tid)
# Update status transaksi jadi 'dibayar'
transaksi_update_status(tid, 'dibayar')
# Tutup meja
meja_tutup(meja)
# Tampilkan struk
self.show_struk(struk)
# Reload data
self.reload_payment_orders()
# Reset selection
self.selected_transaksi_label.config(text="Belum ada transaksi dipilih", foreground='red')
self.selected_total_label.config(text="Total: Rp 0")
self.payment_detail_text.delete('1.0', tk.END)
else:
messagebox.showerror("Pembayaran Gagal", message)
def process_cash_payment(self, transaksi_id, total):
"""Proses pembayaran cash"""
try:
cash_input = float(self.cash_amount_var.get() or 0)
except:
return False, "Jumlah bayar tidak valid"
if cash_input < total:
return False, f"Uang kurang! Total: Rp {total:,.0f}, Bayar: Rp {cash_input:,.0f}"
# Simpan pembayaran
pembayaran_add(transaksi_id, 'cash', cash_input, 'sukses', '')
return True, "Pembayaran cash berhasil"
def process_qris_payment(self, transaksi_id, total):
"""Proses pembayaran QRIS (simulasi)"""
# Konfirmasi
confirm = messagebox.askyesno("Konfirmasi QRIS",
f"Pembayaran QRIS sebesar Rp {total:,.0f}\n\n"
f"⚠️ SIMULASI: Anggap customer sudah scan QR Code\n\n"
f"Lanjutkan pembayaran?")
if not confirm:
return False, "Pembayaran dibatalkan"
# Simpan pembayaran
pembayaran_add(transaksi_id, 'qris', total, 'sukses', '')
return True, "Pembayaran QRIS berhasil"
def process_ewallet_payment(self, transaksi_id, total):
"""Proses pembayaran E-Wallet (simulasi)"""
ewallet_type = self.ewallet_type_var.get()
ewallet_name = ewallet_type.upper()
# Konfirmasi
confirm = messagebox.askyesno("Konfirmasi E-Wallet",
f"Pembayaran {ewallet_name} sebesar Rp {total:,.0f}\n\n"
f"⚠️ SIMULASI: Anggap customer sudah konfirmasi di app\n\n"
f"Lanjutkan pembayaran?")
if not confirm:
return False, "Pembayaran dibatalkan"
# Simpan pembayaran
pembayaran_add(transaksi_id, f'ewallet-{ewallet_type}', total, 'sukses', '')
return True, f"Pembayaran {ewallet_name} berhasil"
def generate_struk(self, transaksi_id):
"""Generate struk transaksi"""
transaksi_data = transaksi_get(transaksi_id)
if not transaksi_data:
return ""
tid, uid, meja, total, status, promo_code, subtotal, item_disc, promo_disc, tanggal = transaksi_data
detail_items = detail_transaksi_list(transaksi_id)
payment_data = pembayaran_get_by_transaksi(transaksi_id)
struk = "═════════════════════════════════════════\n"
struk += " CAFE TOTORO MANIA\n"
struk += " Jl. Raya Kampus No. 123, Surabaya\n"
struk += " Telp: 031-123456\n"
struk += "═════════════════════════════════════════\n\n"
struk += f"No. Transaksi : {tid}\n"
struk += f"Tanggal : {tanggal}\n"
struk += f"Meja : {meja}\n"
struk += f"Kasir : {self.session['username']}\n"
if payment_data:
metode = payment_data[1]
struk += f"Pembayaran : {metode.upper()}\n"
struk += "─────────────────────────────────────────\n"
struk += "ITEM PESANAN:\n"
struk += "─────────────────────────────────────────\n"
for detail in detail_items:
did, mid, qty, harga, subtotal_item = detail
menu_data = menu_get(mid)
if menu_data:
_, nama, kategori, _, _, _, _, _ = menu_data
struk += f"{nama}\n"
struk += f" {qty} x Rp {harga:,.0f}".ljust(30)
struk += f"Rp {subtotal_item:,.0f}\n"
struk += "─────────────────────────────────────────\n"
struk += f"Subtotal : Rp {subtotal:,.0f}\n"
if item_disc > 0:
struk += f"Diskon Item : Rp {item_disc:,.0f}\n"
if promo_disc > 0:
struk += f"Diskon Promo : Rp {promo_disc:,.0f}\n"
struk += "─────────────────────────────────────────\n"
struk += f"TOTAL BAYAR : Rp {total:,.0f}\n"
# Jika cash, tampilkan bayar & kembalian
if payment_data and payment_data[1] == 'cash':
jumlah_bayar = payment_data[2]
kembalian = jumlah_bayar - total
struk += f"Bayar : Rp {jumlah_bayar:,.0f}\n"
struk += f"Kembalian : Rp {kembalian:,.0f}\n"
struk += "═════════════════════════════════════════\n"
struk += " TERIMA KASIH ATAS KUNJUNGAN ANDA\n"
struk += " SAMPAI JUMPA LAGI!\n"
struk += "═════════════════════════════════════════\n"
return struk
def show_struk(self, struk):
"""Tampilkan struk dalam popup window"""
w = tk.Toplevel(self.root)
w.title("✅ Pembayaran Berhasil - Struk Transaksi")
w.geometry("500x600")
w.transient(self.root)
w.grab_set()
# Frame untuk struk
frm = ttk.Frame(w, padding=15)
frm.pack(fill='both', expand=True)
ttk.Label(frm, text="✅ PEMBAYARAN BERHASIL!", font=("Arial", 14, "bold"), foreground='green').pack(pady=10)
# Text widget untuk struk
struk_text = tk.Text(frm, width=50, height=28, font=("Courier New", 9), wrap='word')
struk_scroll = ttk.Scrollbar(frm, orient='vertical', command=struk_text.yview)
struk_text.configure(yscrollcommand=struk_scroll.set)
struk_text.insert('1.0', struk)
struk_text.config(state='disabled') # Read-only
struk_text.pack(side='left', fill='both', expand=True)
struk_scroll.pack(side='right', fill='y')
# Tombol
btn_frame = ttk.Frame(w)
btn_frame.pack(pady=10)
ttk.Button(btn_frame, text="🖨️ Print (Simulasi)", command=lambda: self.print_struk_simulation(struk)).pack(side='left', padx=6)
ttk.Button(btn_frame, text="✅ Tutup", command=w.destroy).pack(side='left', padx=6)
def print_struk_simulation(self, struk):
"""Simulasi print struk (save to file)"""
from datetime import datetime
filename = f"struk_{datetime.now().strftime('%Y%m%d_%H%M%S')}.txt"
try:
with open(filename, 'w', encoding='utf-8') as f:
f.write(struk)
messagebox.showinfo("Print Simulasi", f"Struk berhasil disimpan ke file:\n{filename}")
except Exception as e:
messagebox.showerror("Error", f"Gagal menyimpan struk: {e}")
# Done
if __name__ == "__main__":
init_db_csv()
root = tk.Tk()

View File

@ -1,5 +1,5 @@
id,nama,kategori,harga,stok,foto,tersedia,item_discount_pct
1,Americano,Minuman,20000,5,,1,0
1,Americano,Minuman,20000,4,,1,0
2,Latte,Minuman,25000,1,,1,10
3,Banana Cake,Dessert,30000,0,,0,0
4,Nasi Goreng,Makanan,35000,0,,0,0

1 id nama kategori harga stok foto tersedia item_discount_pct
2 1 Americano Minuman 20000 5 4 1 0
3 2 Latte Minuman 25000 1 1 10
4 3 Banana Cake Dessert 30000 0 0 0
5 4 Nasi Goreng Makanan 35000 0 0 0

View File

@ -1,2 +1,2 @@
id,user_id,nomor_meja,total,status,promo_code,subtotal,item_discount,promo_discount,tanggal
1,4,1,20000.0,dibayar,,20000.0,0.0,0.0,2025-12-13 15:09:50

1 id user_id nomor_meja total status promo_code subtotal item_discount promo_discount tanggal
2 1 4 1 20000.0 dibayar 20000.0 0.0 0.0 2025-12-13 15:09:50