From 123269fc23f3b2edcccb52d9defe6ce032caee41 Mon Sep 17 00:00:00 2001 From: Jevinca Marvella Date: Sat, 13 Dec 2025 15:16:01 +0700 Subject: [PATCH] Pembayaran --- detail_transaksi.csv | 2 +- favorite.csv | 3 +- main.py | 513 ++++++++++++++++++++++++++++++++++++++++++- menu.csv | 2 +- transaksi.csv | 2 +- 5 files changed, 516 insertions(+), 6 deletions(-) diff --git a/detail_transaksi.csv b/detail_transaksi.csv index da34181..0ca8be3 100644 --- a/detail_transaksi.csv +++ b/detail_transaksi.csv @@ -1,2 +1,2 @@ id,transaksi_id,menu_id,qty,harga_satuan,subtotal_item - +1,1,1,1,20000.0,20000.0 diff --git a/favorite.csv b/favorite.csv index 3af07ad..7118a5f 100644 --- a/favorite.csv +++ b/favorite.csv @@ -1 +1,2 @@ -user_id,menu_id,order_count,last_ordered \ No newline at end of file +user_id,menu_id,order_count,last_ordered +4,1,1,2025-12-13 15:09:50 diff --git a/main.py b/main.py index 1ad58c2..6dbdf0d 100644 --- a/main.py +++ b/main.py @@ -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("<>", 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() diff --git a/menu.csv b/menu.csv index b25386d..1e52d66 100644 --- a/menu.csv +++ b/menu.csv @@ -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 diff --git a/transaksi.csv b/transaksi.csv index 3798792..dc3a07e 100644 --- a/transaksi.csv +++ b/transaksi.csv @@ -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