from abc import ABC, abstractmethod from datetime import datetime, timedelta from enum import Enum import uuid import pickle import os # ============================================================================== # Definisi Enum untuk Status # ============================================================================== class StatusItem(Enum): """Enum untuk status item di perpustakaan.""" TERSEDIA = "Tersedia" DIPINJAM = "Dipinjam" HILANG = "Hilang" class StatusTransaksi(Enum): """Enum untuk status transaksi peminjaman.""" AKTIF = "Aktif" SELESAI = "Selesai" TERLAMBAT = "Terlambat" # ============================================================================== # Hierarki Kelas Item Pustaka (LoanableItem, Buku, Majalah) # ============================================================================== class LoanableItem(ABC): """ Kelas abstrak untuk semua item yang dapat dipinjam di perpustakaan. Ini adalah superclass (induk) untuk Buku, Majalah, dll. """ def __init__(self, item_id: str, judul: str): self._item_id = item_id self._judul = judul self._status = StatusItem.TERSEDIA @abstractmethod def get_info(self) -> dict: """Metode abstrak untuk mendapatkan informasi detail dari item.""" pass def ubah_status(self, status_baru: StatusItem): """Mengubah status ketersediaan item.""" self._status = status_baru print(f"INFO: Status '{self._judul}' telah diubah menjadi {status_baru.value}.") @property def item_id(self): return self._item_id @property def judul(self): return self._judul @property def status(self): return self._status class Buku(LoanableItem): """Kelas turunan untuk merepresentasikan sebuah buku.""" def __init__(self, item_id: str, judul: str, pengarang: str): super().__init__(item_id, judul) self._pengarang = pengarang def get_info(self) -> dict: """Implementasi spesifik untuk menampilkan info buku (method overriding).""" return { "ID": self._item_id, "Judul": self._judul, "Tipe": "Buku", "Pengarang": self._pengarang, "Status": self._status.value } class Majalah(LoanableItem): """Kelas turunan untuk merepresentasikan sebuah majalah/jurnal.""" def __init__(self, item_id: str, judul: str, edisi: str): super().__init__(item_id, judul) self._edisi = edisi def get_info(self) -> dict: """Implementasi spesifik untuk menampilkan info majalah (method overriding).""" return { "ID": self._item_id, "Judul": self._judul, "Tipe": "Majalah", "Edisi": self._edisi, "Status": self._status.value } # ============================================================================== # Kelas-kelas Utama Sistem # ============================================================================== class Member: """Merepresentasikan anggota perpustakaan.""" def __init__(self, nama: str, alamat: str): self._member_id = "MEM-" + str(uuid.uuid4())[:8].upper() self._nama = nama self._alamat = alamat @property def member_id(self): return self._member_id @property def nama(self): return self._nama def get_riwayat_peminjaman(self, perpus: 'Perpustakaan') -> list['Transaksi']: """Mendapatkan daftar transaksi yang pernah dilakukan oleh member.""" riwayat = [] for transaksi in perpus.daftar_transaksi: if transaksi.member.member_id == self._member_id: riwayat.append(transaksi) return riwayat class Transaksi: """Merepresentasikan sebuah transaksi peminjaman.""" def __init__(self, member: Member, item: LoanableItem): self._transaksi_id = "TRX-" + str(uuid.uuid4())[:8].upper() self._member = member self._item = item self._tgl_pinjam = datetime.now() self._tgl_kembali = self._tgl_pinjam + timedelta(days=14) # Batas peminjaman 14 hari self._status = StatusTransaksi.AKTIF self._denda = 0.0 @property def transaksi_id(self): return self._transaksi_id @property def member(self): return self._member @property def item(self): return self._item @property def status(self): return self._status def hitung_denda(self) -> float: """Menghitung denda jika pengembalian terlambat.""" if datetime.now() > self._tgl_kembali: terlambat = (datetime.now() - self._tgl_kembali).days self._denda = float(terlambat * 1000) # Denda 1000 per hari self._status = StatusTransaksi.TERLAMBAT return self._denda def selesaikan_transaksi(self): self.hitung_denda() if self._status != StatusTransaksi.TERLAMBAT: self._status = StatusTransaksi.SELESAI class Perpustakaan: """Kelas utama yang mengelola seluruh sistem perpustakaan.""" # Kode sebelumnya: # def __init__(self): # self._daftar_item = [] # self._daftar_member = [] # self._daftar_transaksi = [] # print("Sistem Perpustakaan berhasil dibuat.") def __init__(self, nama_file_data="perpustakaan.pkl"): """ Inisialisasi perpustakaan. Akan mencoba memuat data dari file jika ada. """ self._nama_file_data = nama_file_data # Coba muat data dulu if not self.muat_data(): # Jika gagal (file tidak ada), buat list kosong print("INFO: File data tidak ditemukan. Membuat database baru...") self._daftar_item = [] self._daftar_member = [] self._daftar_transaksi = [] print("Sistem Perpustakaan berhasil dibuat.") @property def daftar_item(self): return self._daftar_item @property def daftar_member(self): return self._daftar_member @property def daftar_transaksi(self): return self._daftar_transaksi def muat_data(self) -> bool: """ Memuat state perpustakaan (daftar item, member, transaksi) dari file pickle. Mengembalikan True jika berhasil, False jika gagal. """ if not os.path.exists(self._nama_file_data): return False # File tidak ada, gagal memuat try: # Buka file dalam mode Read Binary ('rb') with open(self._nama_file_data, 'rb') as f: # Muat data yang disimpan data_dimuat = pickle.load(f) # Kembalikan state objek dari dictionary self._daftar_item = data_dimuat.get("items", []) self._daftar_member = data_dimuat.get("members", []) self._daftar_transaksi = data_dimuat.get("transaksi", []) print(f"INFO: Data perpustakaan berhasil dimuat dari {self._nama_file_data}.") return True except Exception as e: print(f"ERROR saat memuat data: {e}") return False def simpan_data(self): """ Menyimpan state perpustakaan (daftar item, member, transaksi) ke dalam file pickle. """ # Siapkan data yang akan disimpan dalam format dictionary data_untuk_disimpan = { "items": self._daftar_item, "members": self._daftar_member, "transaksi": self._daftar_transaksi } try: # Buka file dalam mode Write Binary ('wb') with open(self._nama_file_data, 'wb') as f: # Simpan data menggunakan pickle pickle.dump(data_untuk_disimpan, f) print(f"\nINFO: Data perpustakaan berhasil disimpan ke {self._nama_file_data}.") except Exception as e: print(f"ERROR saat menyimpan data: {e}") def tambah_item(self, item: LoanableItem): """Menambahkan item baru (buku/majalah) ke perpustakaan.""" self._daftar_item.append(item) print(f"ITEM DITAMBAHKAN: '{item.judul}' ({item.__class__.__name__}) telah ditambahkan ke koleksi.") def tambah_member(self, nama: str, alamat: str) -> Member: """Mendaftarkan anggota baru.""" member_baru = Member(nama, alamat) self._daftar_member.append(member_baru) print(f"MEMBER BARU: {nama} dengan ID {member_baru.member_id} berhasil terdaftar.") return member_baru def cari_item(self, item_id: str) -> LoanableItem | None: """Mencari item berdasarkan ID.""" for item in self._daftar_item: if item.item_id == item_id: return item return None def cari_member(self, member_id: str) -> Member | None: """Mencari member berdasarkan ID.""" for member in self._daftar_member: if member.member_id == member_id: return member return None def cari_transaksi(self, transaksi_id: str) -> Transaksi | None: """Mencari transaksi berdasarkan ID.""" for trx in self._daftar_transaksi: if trx.transaksi_id == transaksi_id: return trx return None def pinjamkan_item(self, member_id: str, item_id: str) -> Transaksi: """Memproses peminjaman item oleh member.""" member = self.cari_member(member_id) item = self.cari_item(item_id) if not member: raise Exception("ERROR: Member tidak ditemukan.") if not item: raise Exception("ERROR: Item tidak ditemukan.") if item.status != StatusItem.TERSEDIA: raise Exception(f"ERROR: Item '{item.judul}' sedang tidak tersedia.") transaksi_baru = Transaksi(member, item) self._daftar_transaksi.append(transaksi_baru) item.ubah_status(StatusItem.DIPINJAM) print(f"TRANSAKSI PEMINJAMAN: '{item.judul}' dipinjam oleh {member.nama}. ID Transaksi: {transaksi_baru.transaksi_id}") return transaksi_baru def terima_pengembalian(self, transaksi_id: str): """Memproses pengembalian item.""" transaksi = self.cari_transaksi(transaksi_id) if not transaksi: raise Exception("ERROR: Transaksi tidak ditemukan.") if transaksi.status != StatusTransaksi.AKTIF: raise Exception("ERROR: Transaksi ini sudah tidak aktif.") transaksi.selesaikan_transaksi() item = transaksi.item item.ubah_status(StatusItem.TERSEDIA) denda = transaksi.hitung_denda() print(f"TRANSAKSI PENGEMBALIAN: '{item.judul}' telah dikembalikan oleh {transaksi.member.nama}.") if denda > 0: print(f" -> Dikenakan denda sebesar: Rp{denda:,.2f}") else: print(" -> Pengembalian tepat waktu.") # Modifikasi Kelas Pustakawan class Pustakawan: """Merepresentasikan pustakawan yang mengelola perpustakaan.""" def __init__(self, pustakawan_id: str, nama: str): self._pustakawan_id = pustakawan_id self._nama = nama def lakukan_pendaftaran(self, perpus: Perpustakaan, nama: str, alamat: str) -> Member: """Wrapper method untuk mendaftarkan member.""" print(f"\n[Aksi Pustakawan: {self._nama}] Mendaftarkan anggota baru...") return perpus.tambah_member(nama, alamat) # Versi Baru dengan Factory def tambah_item_ke_koleksi(self, perpus: Perpustakaan, item_type: str, item_id: str, judul: str, **kwargs): """Wrapper method untuk menambah item baru menggunakan Factory.""" print(f"\n[Aksi Pustakawan: {self._nama}] Meminta pembuatan dan penambahan item '{judul}'...") try: # Panggil Factory untuk membuat objek item_baru = ItemFactory.create_item(item_type, item_id, judul, **kwargs) # Tambahkan objek yang sudah jadi ke perpustakaan perpus.tambah_item(item_baru) except ValueError as e: print(f"ERROR saat menambah item: {e}") except Exception as e: print(f"ERROR tidak terduga: {e}") def lakukan_peminjaman(self, perpus: Perpustakaan, member_id: str, item_id: str) -> Transaksi: """Wrapper method untuk melakukan peminjaman.""" print(f"\n[Aksi Pustakawan: {self._nama}] Memproses peminjaman...") return perpus.pinjamkan_item(member_id, item_id) def lakukan_pengembalian(self, perpus: Perpustakaan, transaksi_id: str): """Wrapper method untuk melakukan pengembalian.""" print(f"\n[Aksi Pustakawan: {self._nama}] Memproses pengembalian...") perpus.terima_pengembalian(transaksi_id) # Kelas Baru: ItemFactory class ItemFactory: """Kelas Factory untuk membuat instance LoanableItem.""" @staticmethod def create_item(item_type: str, item_id: str, judul: str, **kwargs) -> LoanableItem: """ Membuat objek Buku atau Majalah berdasarkan item_type. kwargs bisa berisi 'pengarang' untuk Buku atau 'edisi' untuk Majalah. """ if item_type.lower() == "buku": pengarang = kwargs.get('pengarang', 'N/A') # Ambil pengarang, default N/A if pengarang == 'N/A': print("WARNING: Pengarang tidak disediakan untuk buku.") return Buku(item_id, judul, pengarang) elif item_type.lower() == "majalah": edisi = kwargs.get('edisi', 'N/A') # Ambil edisi, default N/A if edisi == 'N/A': print("WARNING: Edisi tidak disediakan untuk majalah.") return Majalah(item_id, judul, edisi) # Bisa tambahkan tipe lain di sini (misal: Artikel) # elif item_type.lower() == "artikel": # volume = kwargs.get('volume', 0) # return Artikel(item_id, judul, volume) else: raise ValueError(f"Tipe item tidak dikenal: {item_type}") # ============================================================================== # CONTOH PENGGUNAAN PROGRAM (MAIN) # ============================================================================== if __name__ == "__main__": print("===== MEMULAI SIMULASI SISTEM PERPUSTAKAAN =====\n") # 1. Inisialisasi sistem (Otomatis MUAT DATA jika file ada) nama_file_db = "perpus_data.pkl" perpustakaan_utama = Perpustakaan(nama_file_db) # 2. Menambahkan pustakawan pustakawan_andi = Pustakawan("P-001", "Andi") member_budi = None member_citra = None transaksi1 = None transaksi2 = None # 3. Pustakawan mendaftarkan member baru if len(perpustakaan_utama.daftar_member) == 0: print("\n--- Menjalankan setup data baru ---") member_budi = pustakawan_andi.lakukan_pendaftaran(perpustakaan_utama, "Budi Santoso", "Jl. Merdeka No. 10") member_citra = pustakawan_andi.lakukan_pendaftaran(perpustakaan_utama, "Citra Lestari", "Jl. Pahlawan No. 5") # 4. Pustakawan menambahkan item baru pustakawan_andi.tambah_item_ke_koleksi(perpustakaan_utama, "Buku", "BK-001", "OOP Python", pengarang="Dr. Inovatif") pustakawan_andi.tambah_item_ke_koleksi(perpustakaan_utama, "Majalah", "MGZ-001", "Quanta", edisi="Okt 2025") pustakawan_andi.tambah_item_ke_koleksi(perpustakaan_utama, "Buku", "BK-002", "Sains Data", pengarang="Prof. Analitika") else: print("--- Data sudah dimuat dari file ---") if len(perpustakaan_utama.daftar_member) > 0: member_budi = perpustakaan_utama.daftar_member[0] if len(perpustakaan_utama.daftar_member) > 1: member_citra = perpustakaan_utama.daftar_member[1] if not member_budi or not member_citra: print("ERROR: Gagal menginisialisasi data member. Simulasi dihentikan.") exit() print("\n===== STATUS KOLEKSI DAN MEMBER SAAT INI =====\n") print("--- Daftar Item ---") # Contoh polimorfisme saat menampilkan info for item in perpustakaan_utama.daftar_item: print(item.get_info()) print("\n--- Daftar Member ---") for member in perpustakaan_utama.daftar_member: print(f"ID: {member.member_id}, Nama: {member.nama}") # 5. Simulasi Transaksi Peminjaman print("\n===== SIMULASI TRANSAKSI =====") try: trx_budi_aktif = next((trx for trx in perpustakaan_utama.daftar_transaksi if trx.member.member_id == member_budi.member_id and trx.item.item_id == "BK-001" and trx.status == StatusTransaksi.AKTIF), None) if trx_budi_aktif: print(f"INFO: Budi sudah meminjam 'OOP Python' (ID: {trx_budi_aktif.transaksi_id}).") transaksi1 = trx_budi_aktif else: # Budi meminjam buku OOP transaksi1 = pustakawan_andi.lakukan_peminjaman(perpustakaan_utama, member_budi.member_id, "BK-001") trx_citra_aktif = next((trx for trx in perpustakaan_utama.daftar_transaksi if trx.member.member_id == member_citra.member_id and trx.item.item_id == "MGZ-001" and trx.status == StatusTransaksi.AKTIF), None) if trx_citra_aktif: print(f"INFO: Citra sudah meminjam 'Quanta' (ID: {trx_citra_aktif.transaksi_id}).") transaksi2 = trx_citra_aktif else: # Citra meminjam majalah sains transaksi2 = pustakawan_andi.lakukan_peminjaman(perpustakaan_utama, member_citra.member_id, "MGZ-001") # Budi mencoba meminjam buku yang sama lagi (akan gagal) pustakawan_andi.lakukan_peminjaman(perpustakaan_utama, member_budi.member_id, "BK-001") except Exception as e: print(e) print("\n===== STATUS SETELAH PEMINJAMAN =====\n") for item in perpustakaan_utama.daftar_item: print(f"Judul: {item.judul}, Status: {item.status.value}") # 6. Simulasi Transaksi Pengembalian try: if transaksi1 and (transaksi1.status == StatusTransaksi.AKTIF or transaksi1.status == StatusTransaksi.TERLAMBAT): # Budi mengembalikan buku OOP pustakawan_andi.lakukan_pengembalian(perpustakaan_utama, transaksi1.transaksi_id) else: print(f"\nINFO: Transaksi {transaksi1.transaksi_id if transaksi1 else 'Budi'} (OOP Python) sudah dikembalikan sebelumnya.") if transaksi2 and (transaksi2.status == StatusTransaksi.AKTIF or transaksi2.status == StatusTransaksi.TERLAMBAT): # Citra mencoba mengembalikan majalah (simulasi terlambat dengan "memajukan" waktu) transaksi2._tgl_kembali = datetime.now() - timedelta(days=3) # Anggap terlambat 3 hari pustakawan_andi.lakukan_pengembalian(perpustakaan_utama, transaksi2.transaksi_id) else: print(f"\nINFO: Transaksi {transaksi2.transaksi_id if transaksi2 else 'Citra'} (Quanta) sudah dikembalikan sebelumnya.") except Exception as e: print(f"ERROR SIMULASI PENGEMBALIAN: {e}") print("\n===== STATUS AKHIR SETELAH PENGEMBALIAN =====\n") for item in perpustakaan_utama.daftar_item: print(f"Judul: {item.judul}, Status: {item.status.value}") print("\n===== RIWAYAT PEMINJAMAN BUDI =====\n") riwayat_budi = member_budi.get_riwayat_peminjaman(perpustakaan_utama) if riwayat_budi: for trx in riwayat_budi: print(f"ID: {trx.transaksi_id}, Item: '{trx.item.judul}', Status: {trx.status.value}") else: print("Tidak ada riwayat peminjaman untuk Budi.") print("\n===== RIWAYAT PEMINJAMAN CITRA =====\n") riwayat_citra = member_citra.get_riwayat_peminjaman(perpustakaan_utama) if riwayat_citra: for trx in riwayat_citra: print(f"ID: {trx.transaksi_id}, Item: '{trx.item.judul}', Status: {trx.status.value}") else: print("Tidak ada riwayat peminjaman untuk Citra.") # 7. Simpan state terakhir sebelum program selesai try: perpustakaan_utama.simpan_data() except Exception as e: print(f"Gagal menyimpan data di akhir sesi: {e}") print("\n===== SIMULASI SELESAI =====")