import sqlite3, math from datetime import datetime from dataclasses import dataclass from pypos.core.base_model import BaseModel from pypos.core.utils.db_helper import connect_sqlite # upgraded: inherit base class def generate_nomer2(counter: int, kasir_username: str) -> str: """ Generator nomor transaksi kedua (nomer2) dengan format timestamp + counter + username. """ timestamp = datetime.now().strftime('%Y%m%d%H%M%S') no_urut = f"{counter:08d}" return f"{timestamp}{no_urut}-{kasir_username}" # upgraded: inherit base class @dataclass class DetailTransaksi(BaseModel): produk_id: int produk_nama: str produk_ord_hrg: float produk_ord_jml: int produk_jenis: str produk_ord_diskon: float satuan: str = "" def __iter__(self): return iter(( self.produk_id, self.produk_nama, self.produk_ord_hrg, self.produk_ord_jml, self.produk_jenis, self.produk_ord_diskon, self.satuan, )) # upgraded: inherit base class class TransaksiModel(BaseModel): def __init__(self, db_path): super().__init__() self.db_path = db_path def _ensure_pembayaran_non_tunai_column(self, cursor): cursor.execute("PRAGMA table_info(transaksi)") cols = [row[1] for row in cursor.fetchall()] if "pembayaran_non_tunai" not in cols: cursor.execute( "ALTER TABLE transaksi ADD COLUMN pembayaran_non_tunai REAL DEFAULT 0" ) def _build_autocomplete_sql(self, include_trash=True, include_price_filters=True): produk_filter = "COALESCE(p.trash, 0) = 0 AND " if include_trash else "" price_filter_parts = ["pr.produk_id = p.id", "pr.jenis_value = 'harga_list'"] if include_price_filters: price_filter_parts.append("COALESCE(pr.status, 1) = 1") price_filter_parts.append("COALESCE(pr.trash, 0) = 0") price_filter = " AND ".join(price_filter_parts) return f""" SELECT p.id, p.nama, p.barcode, COALESCE(( SELECT pr.nilai FROM price pr WHERE {price_filter} ORDER BY pr.id DESC LIMIT 1 ), 0) AS harga_jual FROM produk p WHERE {produk_filter}{{where_clause}} ORDER BY {{order_clause}} LIMIT ? """ def _fetch_autocomplete_rows(self, where_clause, order_clause, params): conn = connect_sqlite(self.db_path) conn.row_factory = sqlite3.Row cursor = conn.cursor() base_params = tuple(params or ()) sql_variants = [ self._build_autocomplete_sql(include_trash=True, include_price_filters=True), self._build_autocomplete_sql(include_trash=False, include_price_filters=True), self._build_autocomplete_sql(include_trash=False, include_price_filters=False), ] try: for sql_tpl in sql_variants: sql = sql_tpl.format(where_clause=where_clause, order_clause=order_clause) try: cursor.execute(sql, base_params) return cursor.fetchall() except sqlite3.OperationalError: continue return [] finally: conn.close() def _to_autocomplete_payload(self, rows): barang_list = [] mapping = {} for row in rows: harga_jual = row["harga_jual"] if row["harga_jual"] is not None else 0 text = f"{row['nama']} - {harga_jual}" if text in mapping: continue barang_list.append(text) mapping[text] = { "id": row["id"], "nama": row["nama"], "barcode": row["barcode"], "harga_jual": harga_jual, } return barang_list, mapping def get_produk_autocomplete(self, keyword="", limit=50): key = str(keyword or "").strip() try: safe_limit = max(5, int(limit or 50)) except Exception: safe_limit = 50 if not key: rows = self._fetch_autocomplete_rows( where_clause="1=1", order_clause="p.nama ASC", params=(safe_limit,), ) return self._to_autocomplete_payload(rows) exact_key = key nama_prefix_start = key nama_prefix_end = f"{key}\uffff" contains_key = f"%{key}%" numeric_key = "".join(ch for ch in key if ch.isdigit()) barcode_prefix_start = numeric_key if numeric_key else key barcode_prefix_end = f"{barcode_prefix_start}\uffff" where_clause = ( "(" "(p.nama >= ? AND p.nama < ?) OR " "(p.barcode >= ? AND p.barcode < ?) OR " "p.nama = ? OR p.barcode = ?" ")" ) order_clause = ( "CASE " "WHEN p.barcode = ? THEN 0 " "WHEN p.nama = ? THEN 1 " "WHEN (p.barcode >= ? AND p.barcode < ?) THEN 2 " "WHEN (p.nama >= ? AND p.nama < ?) THEN 3 " "ELSE 4 END, p.nama ASC" ) rows = self._fetch_autocomplete_rows( where_clause=where_clause, order_clause=order_clause, params=( nama_prefix_start, nama_prefix_end, barcode_prefix_start, barcode_prefix_end, exact_key, exact_key, exact_key, exact_key, barcode_prefix_start, barcode_prefix_end, nama_prefix_start, nama_prefix_end, safe_limit, ), ) if len(rows) < safe_limit and len(key) >= 3: existing_ids = {str(row["id"]) for row in rows} remaining = safe_limit - len(rows) if existing_ids: placeholders = ",".join(["?"] * len(existing_ids)) exclude_clause = f" AND p.id NOT IN ({placeholders})" extra_params = tuple(existing_ids) else: exclude_clause = "" extra_params = () contains_where = f"(p.nama LIKE ? OR p.barcode LIKE ?){exclude_clause}" contains_rows = self._fetch_autocomplete_rows( where_clause=contains_where, order_clause="p.nama ASC", params=(contains_key, contains_key) + extra_params + (remaining,), ) rows.extend(contains_rows) return self._to_autocomplete_payload(rows[:safe_limit]) def cari_barang_by_id(self, id_produk, jumlah_beli): conn = connect_sqlite(self.db_path) conn.row_factory = sqlite3.Row cursor = conn.cursor() result = { "id": id_produk, "barcode": None, "nama": None, "harga": None, "harga_normal": None, "flag_diskon_grosir": 0, "flag_diskon_free": 0, "diskon_persen": 0, "keterangan_diskon": "", "keterangan_diskon_free": "", "free_produk_id": None, "free_produk_nama": None, "kelipatan": None, "jumlah_free": 0, "jumlah": jumlah_beli, "hpp": None, "satuan": None, } try: cursor.execute(""" SELECT produk.nama, diskon.persen, diskon.nilai, diskon.harga, (diskon.nilai + diskon.harga) AS harga_setelah_diskon,produk.barcode,produk.hpp,produk.satuan FROM produk INNER JOIN diskon ON produk.id = diskon.produk_id WHERE produk.id = ? AND diskon.jenis = 'produk_grosir' ORDER BY diskon.id DESC LIMIT 1 """, (id_produk,)) grosir = cursor.fetchone() if grosir: self.log_debug( f"aktifkan flag diskon grosir id_produk={id_produk} harga={grosir['harga_setelah_diskon']}" ) result.update({ "nama": grosir["nama"], "flag_diskon_grosir": 1, "harga": grosir["harga_setelah_diskon"], "diskon_persen": 0, "keterangan_diskon": f"Diskon 0 % = 0", "barcode": grosir["barcode"], "hpp": grosir["hpp"], "satuan": grosir["satuan"], }) cursor.execute(""" SELECT produk.nama, diskon.persen, diskon.nilai, diskon.harga, (diskon.nilai + diskon.harga) AS harga_setelah_diskon,produk.barcode,produk.hpp,produk.satuan FROM produk INNER JOIN diskon ON produk.id = diskon.produk_id WHERE produk.id = ? AND diskon.jenis = 'produk_grosir' AND ? BETWEEN diskon.minim AND COALESCE(NULLIF(diskon.maxim, 0), 10000) ORDER BY diskon.id DESC LIMIT 1 """, (id_produk, jumlah_beli)) nominal_grosir = cursor.fetchone() if nominal_grosir: result.update({ "nama": nominal_grosir["nama"], "flag_diskon_grosir": 1, "harga": nominal_grosir["harga_setelah_diskon"], "diskon_persen": nominal_grosir["persen"], "keterangan_diskon": f"Diskon {round(nominal_grosir['persen'])}% = {round(nominal_grosir['nilai'])}", "barcode": nominal_grosir["barcode"], "hpp": nominal_grosir["hpp"], "satuan": nominal_grosir["satuan"], }) cursor.execute(""" SELECT diskon.free_produk_nama, diskon.kelipatan, diskon.minim , produk.barcode,produk.hpp,produk.satuan FROM produk INNER JOIN diskon ON produk.id = diskon.produk_id WHERE produk.id = ? AND diskon.jenis = 'free_produk' AND DATE('now') BETWEEN diskon.dtime_start AND diskon.dtime_end ORDER BY diskon.id DESC LIMIT 1 """, (id_produk,)) free = cursor.fetchone() if free: self.log_debug( f"aktifkan flag diskon free nama={free['free_produk_nama']} kelipatan={free['kelipatan']}" ) result.update({ "flag_diskon_free": 1, "free_produk_nama": free["free_produk_nama"], "kelipatan": free["kelipatan"], "jumlah_free": 0, "barcode": free["barcode"], "hpp": free["hpp"], "satuan": free["satuan"], }) if free["kelipatan"] == 1: result.update({ "keterangan_diskon_free": f'gratis produk {free["free_produk_nama"]} berlaku kelipatan setiap pembelian {free["minim"]}' }) else: result.update({ "keterangan_diskon_free": f'gratis produk {free["free_produk_nama"]} tidak berlaku kelipatan minimal pembelian {free["minim"]}' }) cursor.execute(""" SELECT diskon.free_produk_nama, diskon.kelipatan, diskon.minim , produk.barcode,produk.hpp,produk.satuan FROM produk INNER JOIN diskon ON produk.id = diskon.produk_id WHERE produk.id = ? AND diskon.jenis = 'free_produk' AND DATE('now') BETWEEN diskon.dtime_start AND diskon.dtime_end AND ? BETWEEN diskon.minim AND COALESCE(NULLIF(diskon.maxim, 0), 10000) ORDER BY diskon.id DESC LIMIT 1 """, (id_produk, jumlah_beli)) nominal_free = cursor.fetchone() if nominal_free: self.log_debug("mau update nominal free") result.update({ "flag_diskon_free": 1, "free_produk_nama": nominal_free["free_produk_nama"], "kelipatan": nominal_free["kelipatan"], "jumlah_free": math.floor(jumlah_beli / nominal_free["minim"]), "barcode": nominal_free["barcode"], "hpp": nominal_free["hpp"], "satuan": nominal_free["satuan"], }) if not result["harga"]: cursor.execute(""" SELECT p.nama, MAX(CASE WHEN price.jenis_value = 'harga_list' THEN price.nilai END) AS harga_jual,p.barcode,p.hpp,p.satuan FROM produk p INNER JOIN price ON p.id = price.produk_id WHERE p.id = ? GROUP BY p.nama LIMIT 1 """, (id_produk,)) produk = cursor.fetchone() if produk: result.update({ "harga": produk["harga_jual"], "harga_normal": produk["harga_jual"], "nama": produk["nama"], "barcode": produk["barcode"], "hpp": produk["hpp"], "satuan": produk["satuan"], }) except Exception as e: self.log_error(f"DB Error: {e}") finally: conn.close() return result def cari_barang_by_nama(self, nama_barang): conn = connect_sqlite(self.db_path) conn.row_factory = sqlite3.Row cursor = conn.cursor() key = str(nama_barang or "").strip() if not key: conn.close() return None try: cursor.execute("SELECT * FROM produk WHERE nama = ? LIMIT 1", (key,)) row = cursor.fetchone() if not row: cursor.execute( "SELECT * FROM produk WHERE nama >= ? AND nama < ? LIMIT 1", (key, f"{key}\uffff"), ) row = cursor.fetchone() if not row and len(key) >= 3: cursor.execute("SELECT * FROM produk WHERE nama LIKE ? LIMIT 1", (f"%{key}%",)) row = cursor.fetchone() except sqlite3.OperationalError: cursor.execute("SELECT * FROM produk WHERE nama LIKE ? LIMIT 1", (f"%{key}%",)) row = cursor.fetchone() conn.close() return dict(row) if row else None def cari_barang_by_barcode(self, barcode): conn = connect_sqlite(self.db_path) conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute("SELECT * FROM produk WHERE barcode = ?", (barcode,)) row = cursor.fetchone() conn.close() return dict(row) if row else None def get_lookup_query_plan(self, sample_keyword="A", sample_barcode="12345"): keyword = str(sample_keyword or "A").strip() or "A" barcode = str(sample_barcode or "12345").strip() or "12345" conn = connect_sqlite(self.db_path) cursor = conn.cursor() query_map = { "barcode_exact": ( "EXPLAIN QUERY PLAN SELECT id FROM produk WHERE barcode = ? LIMIT 1", (barcode,), ), "nama_exact": ( "EXPLAIN QUERY PLAN SELECT id FROM produk WHERE nama = ? LIMIT 1", (keyword,), ), "nama_prefix": ( "EXPLAIN QUERY PLAN SELECT id FROM produk WHERE nama >= ? AND nama < ? LIMIT 1", (keyword, f"{keyword}\uffff"), ), "nama_contains": ( "EXPLAIN QUERY PLAN SELECT id FROM produk WHERE nama LIKE ? LIMIT 1", (f"%{keyword}%",), ), } result = {} try: for key, payload in query_map.items(): sql, params = payload try: cursor.execute(sql, params) rows = cursor.fetchall() details = [str(row[3]) for row in rows if len(row) > 3] result[key] = details except Exception as exc: result[key] = [f"error: {exc}"] return result finally: conn.close() def get_and_increment_counter(self, nama="transaksi") -> int: conn = connect_sqlite(self.db_path) cursor = conn.cursor() try: cursor.execute("SELECT counter FROM penomoran WHERE nama = ?", (nama,)) row = cursor.fetchone() if not row: cursor.execute("INSERT INTO penomoran (nama, counter) VALUES (?, ?)", (nama, 0)) counter = 0 else: counter = row[0] new_counter = counter + 1 cursor.execute( "UPDATE penomoran SET counter = ?, dtime_update = datetime('now') WHERE nama = ?", (new_counter, nama), ) conn.commit() return new_counter finally: conn.close() def ambil_diskon_by_produk(self, db_path, produk_id): """Ambil data diskon untuk 1 produk - method legacy""" conn = connect_sqlite(db_path) conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute(""" SELECT nomer_diskon, produk_id, free_produk_id, free_produk_nama, kelipatan, quota_global, quota_used FROM diskon WHERE produk_id = ? AND jenis = 'free_produk' ORDER BY id DESC LIMIT 1 """, (produk_id,)) row = cursor.fetchone() conn.close() if row: return { "nomer_diskon": row["nomer_diskon"], "produk_id": row["produk_id"], "free_produk_id": row["free_produk_id"], "free_produk_nama": row["free_produk_nama"], "kelipatan": row["kelipatan"], "quota_global": row["quota_global"], "quota_used": row["quota_used"], } else: return None def ambil_diskon_batch(self, db_path, produk_ids): """ Ambil diskon untuk multiple produk sekaligus (batch query). """ if not produk_ids: return {} conn = connect_sqlite(db_path) conn.row_factory = sqlite3.Row cursor = conn.cursor() try: placeholders = ",".join(["?"] * len(produk_ids)) cursor.execute(f""" SELECT nomer_diskon, produk_id, free_produk_id, free_produk_nama, kelipatan, quota_global, quota_used FROM diskon WHERE produk_id IN ({placeholders}) AND jenis = 'free_produk' ORDER BY produk_id, id DESC """, produk_ids) rows = cursor.fetchall() diskon_map = {} for row in rows: prod_id = row["produk_id"] if prod_id not in diskon_map: diskon_map[prod_id] = { "nomer_diskon": row["nomer_diskon"], "produk_id": row["produk_id"], "free_produk_id": row["free_produk_id"], "free_produk_nama": row["free_produk_nama"], "kelipatan": row["kelipatan"], "quota_global": row["quota_global"], "quota_used": row["quota_used"], } return diskon_map finally: conn.close() def simpan_transaksi(self, transaksi_data, detail_data, transaksi_data_dict): counter = self.get_and_increment_counter("transaksi") nomer2 = generate_nomer2(counter, transaksi_data_dict["oleh_nama"]) transaksi_data_dict["nomer2"] = nomer2 self.log_debug(f"simpan_transaksi counter={counter}, nomer2={nomer2}") self.log_debug(f"simpan_transaksi transaksi_nilai={transaksi_data_dict['transaksi_nilai']}") self.log_debug(f"transaksi_dibayar={transaksi_data_dict.get('transaksi_dibayar', 'NOT SET')}") self.log_debug( f"transaksi_dibayar_return={transaksi_data_dict.get('transaksi_dibayar_return', 'NOT SET')}" ) conn = connect_sqlite(self.db_path) cursor = conn.cursor() self.log_debug("simpan_transaksi akan memasukkan data ke tabel transaksi master") try: self._ensure_pembayaran_non_tunai_column(cursor) # PATCH[FrontEndAgent|ColumnFix]: Hapus field yang tidak ada di tabel transaksi # Field ini hanya untuk print/display, bukan untuk database insert_dict = dict(transaksi_data_dict) fields_to_remove = ['jumlah_bayar', 'kembalian', 'kasir_nama', 'customer_nama', 'skip_logo'] for field in fields_to_remove: insert_dict.pop(field, None) columns = ", ".join(insert_dict.keys()) placeholders = ", ".join(["?"] * len(insert_dict)) values = list(insert_dict.values()) cursor.execute(f""" INSERT INTO transaksi ({columns}) VALUES ({placeholders}) """, values) transaksi_id = cursor.lastrowid for detail in detail_data: cursor.execute(""" INSERT INTO transaksi_data ( transaksi_id, produk_id, produk_nama, produk_ord_hrg, produk_ord_jml, produk_jenis, produk_ord_diskon, satuan ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) """, detail.to_tuple_with_transaksi_id(transaksi_id)) arr_free_produk = [] free_produk_details = [ d for d in detail_data if d.produk_jenis == "free_produk" and d.produk_ord_jml > 0 ] if free_produk_details: self.log_debug(f"[OPTIMIZED] Proses {len(free_produk_details)} free produk dengan batch query") produk_ids = [d.produk_id for d in free_produk_details] diskon_map = self.ambil_diskon_batch(self.db_path, produk_ids) for detail in free_produk_details: diskon_data = diskon_map.get(detail.produk_id) if not diskon_data: self.log_warning(f"Diskon tidak ditemukan untuk produk {detail.produk_id}") continue barang_detail = self.cari_barang_by_id(detail.produk_id, detail.produk_ord_jml) jumlah_free = barang_detail.get("jumlah_free", 0) free_item = { "diskon_id": diskon_data["nomer_diskon"], "produk_id": detail.produk_id, "produk_nama": detail.produk_nama, "free_produk_id": diskon_data["free_produk_id"], "free_produk_nama": diskon_data["free_produk_nama"], "free_qty": jumlah_free, "kelipatan": diskon_data["kelipatan"], "quota_global": diskon_data["quota_global"], "quota_used": diskon_data["quota_used"], "transaksi_id": transaksi_id, "transaksi_no": transaksi_data_dict["nomer"], "oleh_id": transaksi_data_dict["oleh_id"], "oleh_nama": transaksi_data_dict["oleh_nama"], "customer_id": transaksi_data_dict.get("customers_id", 1), "customer_nama": transaksi_data_dict.get("customers_nama", "Tunai"), } arr_free_produk.append(free_item) self.log_info( f"Free produk item: {free_item['produk_nama']} -> {free_item['free_produk_nama']}" ) conn.commit() return transaksi_id, arr_free_produk except Exception as e: conn.rollback() raise e finally: conn.close() def simpan_transaksi_f9(self, transaksi_data, detail_data): conn = connect_sqlite(self.db_path) cursor = conn.cursor() try: cursor.execute(""" INSERT INTO transaksi ( nomer, dtime, transaksi_nilai, diskon_persen, ppn_persen, transaksi_bulat, customers_id, customers_nama, fulldate, oleh_id, oleh_nama, jenis_label, transaksi_jenis, settlement_id ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) """, transaksi_data) transaksi_id = cursor.lastrowid for detail in detail_data: cursor.execute(""" INSERT INTO transaksi_data ( transaksi_id, produk_id, produk_nama, produk_ord_hrg, produk_ord_jml, produk_jenis, produk_ord_diskon, satuan ) VALUES (?, ?, ?, ?, ?, ?, ?, ?) """, detail.to_tuple_with_transaksi_id(transaksi_id)) conn.commit() return transaksi_id except Exception as e: conn.rollback() raise e finally: conn.close() def preorder_exists(self, nomer): if not nomer: return False try: conn = connect_sqlite(self.db_path) cursor = conn.cursor() cursor.execute( "SELECT 1 FROM transaksi WHERE nomer = ? AND jenis_label = 'simpan_transaksi' AND COALESCE(trash, 0) = 0 LIMIT 1", (nomer,) ) return cursor.fetchone() is not None except Exception: return False finally: try: cursor.close() conn.close() except Exception: pass def get_transaksi_terakhir(self): """ Ambil transaksi terakhir yang SUDAH SELESAI dibayar (bukan simpan_transaksi/F9). Hanya ambil transaksi dengan jenis_label = 'invoice' (transaksi selesai). """ conn = connect_sqlite(self.db_path) conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute(""" SELECT * FROM transaksi WHERE jenis_label = 'invoice' AND COALESCE(trash, 0) = 0 ORDER BY id DESC LIMIT 1 """) transaksi_row = cursor.fetchone() conn.close() return dict(transaksi_row) if transaksi_row else None def get_detail_transaksi(self, transaksi_id): conn = connect_sqlite(self.db_path) conn.row_factory = sqlite3.Row cursor = conn.cursor() cursor.execute("SELECT * FROM transaksi_data WHERE transaksi_id = ? AND COALESCE(trash, 0) = 0", (transaksi_id,)) detail_rows = cursor.fetchall() conn.close() return detail_rows # DATA HELPERS (dipanggil controller) def map_detail_rows(self, detail_rows): """Konversi rows detail ke list DetailTransaksi.""" mapped = [] for row in detail_rows: try: satuan_val = row["satuan"] if "satuan" in row.keys() else "" except Exception: satuan_val = "" mapped.append(DetailTransaksi( produk_id=row["produk_id"], produk_nama=row["produk_nama"], produk_ord_hrg=row["produk_ord_hrg"], produk_ord_jml=row["produk_ord_jml"], produk_jenis=row["produk_jenis"], produk_ord_diskon=row["produk_ord_diskon"], satuan=satuan_val )) return mapped def build_transaksi_dict_from_row(self, transaksi_row): """ Bangun transaksi_data_dict dari row transaksi (dict/sqlite Row). """ get_val = transaksi_row.get if hasattr(transaksi_row, "get") else lambda k, default=None: transaksi_row[k] if k in transaksi_row.keys() else default # PATCH[FrontEndAgent|PrintUnification]: Tambah field untuk print (samakan dengan history + payment) transaksi_dibayar = get_val("transaksi_dibayar", 0) transaksi_dibayar_return = get_val("transaksi_dibayar_return", 0) return { "id": get_val("id"), "nomer": get_val("nomer"), "dtime": get_val("dtime"), "customers_id": get_val("customers_id", 1), "customers_nama": get_val("customers_nama", "Tunai"), "transaksi_nilai": get_val("transaksi_nilai", 0), "transaksi_bulat": get_val("transaksi_bulat", 0), "ppn_persen": get_val("ppn_persen", 0), "diskon_persen": get_val("diskon_persen", 0), "settlement_id": get_val("settlement_id", 1), "oleh_id": get_val("oleh_id"), "oleh_nama": get_val("oleh_nama", ""), "transaksi_dibayar": transaksi_dibayar, "transaksi_dibayar_return": transaksi_dibayar_return, # PATCH: Duplicate untuk kompatibilitas print "jumlah_bayar": transaksi_dibayar, "kembalian": transaksi_dibayar_return, "bank_nama": get_val("bank_nama", ""), "bank_rekening_nama": get_val("bank_rekening_nama", ""), "rekening": get_val("rekening", ""), # PATCH: Field tambahan untuk print "kasir_nama": get_val("oleh_nama", ""), "customer_nama": get_val("customers_nama", "Tunai"), "skip_logo": True, # Skip logo (too large) } def apply_single_payment(self, transaksi_data_dict, hasil_pembayaran): """ Lengkapi transaksi_data_dict dengan info pembayaran tunggal. """ updated = dict(transaksi_data_dict) updated["settlement_id"] = getattr(hasil_pembayaran, "settlement_id", 1) updated["bank_id"] = getattr(hasil_pembayaran, "bank_id", 101) updated["bank_nama"] = getattr(hasil_pembayaran, "bank_nama", "Tunai") updated["transaksi_dibayar"] = getattr(hasil_pembayaran, "jumlah_bayar", hasil_pembayaran.jumlah_dibayar) updated["transaksi_dibayar_return"] = getattr(hasil_pembayaran, "kembalian", hasil_pembayaran.kembali) # PATCH[FrontEndAgent|PrintUnification]: Tambah field untuk print (samakan dengan history) updated["jumlah_bayar"] = updated["transaksi_dibayar"] updated["kembalian"] = updated["transaksi_dibayar_return"] metode_text = hasil_pembayaran.metode if hasattr(hasil_pembayaran, "metode") else "Tunai" metode_norm = str(metode_text).strip().lower() if metode_norm == "tunai": updated["pembayaran_non_tunai"] = 0 else: updated["pembayaran_non_tunai"] = updated["transaksi_dibayar"] return updated, metode_text def apply_multi_payment(self, transaksi_data_dict, payment_list): """ Lengkapi transaksi_data_dict untuk multi pembayaran (split). """ updated = dict(transaksi_data_dict) total_dibayar = sum(p.jumlah_dibayar for p in payment_list) total_kembalian = payment_list[-1].kembali if payment_list else 0 total_tunai = sum( p.jumlah_dibayar for p in payment_list if str(getattr(p, "metode", "")).strip().lower() == "tunai" ) first_payment = payment_list[0] updated["settlement_id"] = getattr(first_payment, "settlement_id", 1) updated["bank_id"] = getattr(first_payment, "bank_id", 101) metode_list = [p.metode for p in payment_list] metode_text = " + ".join(metode_list) updated["bank_nama"] = f"Multi: {metode_text}" updated["transaksi_dibayar"] = total_dibayar updated["transaksi_dibayar_return"] = total_kembalian # PATCH[FrontEndAgent|PrintUnification]: Tambah field untuk print (samakan dengan history) updated["jumlah_bayar"] = total_dibayar updated["kembalian"] = total_kembalian non_tunai = total_dibayar - total_tunai if non_tunai < 0: non_tunai = 0 updated["pembayaran_non_tunai"] = non_tunai return updated, metode_text, total_dibayar, total_kembalian