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 =====")