Otomatisasi Laporan Keuangan dari berbagai macam Invoice (Gratis!)

Otomatisasi Laporan Keuangan: Dari berbagai macam Invoice di GDrive ke Dashboard Looker Studio dengan Gemini API (Gratis!)

Parung Panjang - Mengelola keuangan pribadi maupun bisnis kecil seringkali menjadi tantangan tersendiri. Pencatatan manual setiap transaksi, mengkategorikannya, lalu menyusun laporan bisa memakan waktu berjam-jam dan rentan terhadap kesalahan. Namun, bagaimana jika kami katakan bahwa kalian bisa membangun sistem laporan keuangan otomatis yang canggih, mengambil data langsung dari e-statement atau invoice (Format PDF/Image) di Google Drive kalian, mengolahnya dengan kecerdasan buatan (AI), dan menampilkannya dalam dashboard interaktif yang bisa diakses kapan saja? Dan yang terbaik, semua ini bisa dilakukan dengan memanfaatkan alat-alat yang sebagian besar gratis!

Dalam artikel ini, kami akan memandu kalian langkah demi langkah untuk membangun sistem impian tersebut. Kita akan menggunakan Google Colab sebagai "otak" pemrosesan, Gemini API dari Google sebagai "asisten cerdas" untuk mengekstrak dan mengkategorikan data, Google Sheets sebagai "brankas data" yang rapi, dan Looker Studio untuk "menyulap" data tersebut menjadi visualisasi yang mudah dipahami. Siap untuk mengucapkan selamat tinggal pada entri data manual dan menyambut era laporan keuangan otomatis? Mari kita mulai!

Financial Report


Gambaran Besar Arsitektur: Bagaimana Semuanya Bekerja Bersama?

Sebelum kita menyelami detail teknis, mari kita pahami alur kerja sistem otomatisasi laporan keuangan ini. Bayangkan sebuah alur data yang mengalir mulus dari dokumen mentah hingga menjadi insight visual:

  1. Penyimpanan File Sumber: Kalian menyimpan semua e-statement, invoice, atau bukti transaksi lainnya (terutama dalam format PDF) di folder khusus dalam Google Drive kalian.
  2. Orkestrasi Proses: Sebuah skrip Python yang dijalankan di Google Colab akan menjadi pusat kendali. Skrip ini akan mengakses file-file di Google Drive.
  3. Ekstraksi dan Kategorisasi Cerdas: Untuk setiap file, skrip akan:
    • Jika file berupa PDF, teksnya akan diekstrak menggunakan pustaka pdfminer.six.
    • Untuk mengoptimalkan penggunaan token, teks panjang dari PDF akan dipecah menjadi bagian-bagian kecil menggunakan textwrap.
    • Teks (atau file non-PDF secara keseluruhan) kemudian dikirim ke Gemini API.
    • Dengan prompt yang telah dirancang khusus, Gemini API akan mengekstrak detail transaksi seperti tanggal, deskripsi, jumlah, dan bahkan menyarankan kategori serta sub-kategori.
    • Hasil dari Gemini API adalah data terstruktur dalam format JSON.
  4. Penyimpanan Data Terstruktur: Data JSON ini kemudian diolah dan ditambahkan ke dalam Google Sheets. Skrip akan dilengkapi dengan mekanisme untuk mencegah duplikasi data, memastikan setiap transaksi hanya tercatat sekali.
  5. Visualisasi Dinamis: Looker Studio akan terhubung ke Google Sheets tersebut sebagai sumber datanya. Looker Studio akan secara otomatis memperbarui (refresh) data setiap 15 menit.
  6. Dashboard Interaktif: Hasil akhirnya adalah sebuah dashboard laporan keuangan di Looker Studio yang menampilkan ringkasan, tren, dan detail transaksi kalian secara visual dan interaktif.

Dengan arsitektur ini, proses yang tadinya manual dan memakan waktu menjadi otomatis, efisien, dan memberikan kalian akses ke informasi keuangan terkini dengan cepat.

Kode Python Lengkap di Google Colab

Berikut adalah kode lengkap yang dapat Anda salin dan tempel ke dalam satu sel di Google Colab untuk menjalankan keseluruhan proses.

# @title 🤖 Script Extract Laporan Keuangan
# @markdown ### 1. Konfigurasi Awal
# @markdown Masukkan semua informasi yang dibutuhkan di bawah ini.
# --- Mengimpor Pustaka yang Diperlukan ---
import re
import os
import shutil
import json
import magic
import pandas as pd
from datetime import datetime
import pytz # Import pytz for timezone handling
from google.colab import auth, drive
from IPython.display import display, HTML
import gspread
# from google.auth import default
from google import genai
from pdfminer.high_level import extract_text
import textwrap
# --- Input parameter
model_name = "gemini-1.5-flash"                                                   # @param ["gemini-1.5-flash", "gemini-2.0-flash", "gemini-2.5-pro-exp-03-25"]
api_key = "MASUKKAN_API_KEY_ANDA_DISINI"                               # @param {type:"string"}
sheet_name = "Transaksi"                                                          # @param {type:"string"}
worksheet_name = "Laporan Keuangan"                                               # @param {type:"string"}
raw_data_path = "Learning/Laporan Keuangan/raw_data"                              # @param {type:"string"}
archive_path = "Learning/Laporan Keuangan/archived"                               # @param {type:"string"}
log_file_path = "Learning/Laporan Keuangan/processing.log"                        # @param {type:"string"}
gs_json = "/content/drive/My Drive/Learning/Laporan Keuangan/admin-bayhaqy.json" # @param {type:"string"}
def process_file_with_gemini(file_path, api_key, model_name, log_file_absolute):
    """Mengirim file ke Gemini dan mengurai respons JSON, serta menyimpan respons mentah."""
    file_name = os.path.basename(file_path)
    print(f"Processing file: {file_name}")
    log_message(f"Processing file: {file_name}", log_file_absolute)
    # Safely extract account name from filename
    match = re.search(r'^[a-zA-Z]+', file_name)
    if match:
        clean_file_name = match.group(0).upper()
        print(f"Cleaned file name: {clean_file_name}")
        log_message(f"Cleaned file name: {clean_file_name}", log_file_absolute)
    else:
        # Handle cases where filename does not start with letters
        clean_file_name = file_name
        print(f"Warning: Filename does not start with letters. Using default: {clean_file_name}")
        log_message(f"Warning: Filename does not start with letters. Using default: {clean_file_name}", log_file_absolute)
    log_message(f"🧠 Mengirim file '{file_name}' ke Gemini...", log_file_absolute)
    print(f"🧠 Mengirim file '{file_name}' ke Gemini...")
    try:
        client = genai.Client(api_key=api_key)
        # Mendeteksi tipe file (MIME type)
        mime_type = magic.from_file(file_path, mime=True)
        log_message(f"    Mime Type terdeteksi: {mime_type}", log_file_absolute)
        print(f"    Mime Type terdeteksi: {mime_type}")
        if not mime_type:
             log_message("   ⚠️ Tidak dapat mendeteksi Mime Type, mencoba sebagai application/pdf.", log_file_absolute)
             print("   ⚠️ Tidak dapat mendeteksi Mime Type, mencoba sebagai application/pdf.")
             mime_type = "application/pdf" # Default fallback
        all_extracted_data = [] # List to aggregate data from all pages
        if mime_type == 'application/pdf':
            log_message("Processing as PDF using pdfminer.six for text extraction.", log_file_absolute)
            print("Processing as PDF using pdfminer.six for text extraction.")
            try:
                # Extract text from the entire PDF using pdfminer.six
                full_pdf_text = extract_text(file_path)
                log_message("Successfully extracted text from the entire PDF.", log_file_absolute)
                
                chunk_size = 8000 # Example chunk size (adjust based on testing)
                text_chunks = textwrap.wrap(full_pdf_text, chunk_size, break_long_words=False, replace_whitespace=False)
                log_message(f"Split PDF text into {len(text_chunks)} chunks.", log_file_absolute)
                print(f"Split PDF text into {len(text_chunks)} chunks.")

                for i, chunk in enumerate(text_chunks):
                    log_message(f"Processing text chunk {i + 1}/{len(text_chunks)}", log_file_absolute)
                    print(f"Processing text chunk {i + 1}/{len(text_chunks)}")
                    log_message(f"Sending text chunk {i + 1} to Gemini.", log_file_absolute)
                    print(f"Sending text chunk {i + 1} to Gemini.")
                    prompt = f\"\"\"
                    Anda adalah asisten keuangan ahli yang sangat teliti. Tugas Anda adalah mengekstrak SEMUA data transaksi dari potongan teks ini, yang berasal dari file {clean_file_name}.
                    Ini adalah sebuah fragmen teks, bukan keseluruhan dokumen. Analisis fragmen ini secara independen untuk transaksi di dalamnya.
                    Pastikan untuk tanggal bisa di review secara keseluruhan dokumen agar tidak salah Format "YYYY-MM-DD".
                    Ikuti instruksi ini dengan SANGAT HATI-HATI:
                    1.  Analisis teks fragmen ini dan identifikasi setiap baris transaksi.
                    2.  Untuk setiap transaksi yang ditemukan di fragmen ini, ekstrak informasi berikut (pastikan tidak ada isi column yang membuat jsonnya error seperti kutip / bracket):
                        * `transaksi_id`: Nomor transaksi jika ada, generate otomatis jika tidak ada dengan kombinasi nama akun dan nomor urut.
                        * `tanggal`: Format "YYYY-MM-DD". Jika tahun tidak ada, asumsikan tahun berjalan. Terdakang seperti akun BCA memiliki format DD/MM.
                        * `deskripsi`: Deskripsi lengkap dan jelas dari transaksi. Bersihkan, upper dan lengkapi isinya.
                        * `kategori`: Berikan saran kategori yang paling relevan (contoh: Tempat Tinggal, Kebutuhan Harian, Makanan & Minuman, Hiburan, Investasi, Transportasi, Lain-lain, Pendidikan, Utilitas, Belanja Pribadi, Gaji).
                        * `sub_kategori`: Berikan saran sub-kategori yang lebih spesifik (contoh: Makan Siang, Air, Listrik, Internet, Ojek Online, Belanja Dapur, Pendidikan, Gaji Bulanan).
                        * `tipe`: Harus "Pemasukan" atau "Pengeluaran".
                        * `jumlah`: Angka saja, tanpa titik, koma, atau "Rp".
                        * `akun`: **Gunakan nama file ({clean_file_name}) yang menentukan akun.**. Note: jika tidak ada yang sesuai nama bank/CC/lainnya. Tentukan sebagai BCA/MANDIRI/JAGO/SEABANK/SUPERBANK/GOPAY/OVO/CREDIT CARD/CASH/ETC)
                        * `dari`: Pengirim dari info di deskripsi. Misalnya Bisa orang (seperti "Saya" jika pengeluaran)/bank/dll.
                        * `ke`: Tujuan dari info di deskripsi. Misalnya Bisa orang (seperti "Saya" jika pemasukan)/bank/dll.
                        * `jenisfile`: {mime_type}
                    3.  Keluarkan hasilnya HANYA dalam format list JSON yang valid. **JANGAN tambahkan ```json```, teks pembuka, teks penutup, atau penjelasan apapun di luar JSON.**
                    Contoh JSON format standard:
                    [
                      {{
                        "transaksi_id": "1",
                        "tanggal": "2025-06-05",
                        "deskripsi": "Pembelian di TOKOPEDIA",
                        "kategori": "Belanja Pribadi",
                        "sub_kategori": "Belanja Online",
                        "tipe": "Pengeluaran",
                        "jumlah": 250000,
                        "akun": "Kartu Kredit",
                        "dari": "Kartu Kredit",
                        "ke": "tokopedia",
                        "jenisfile": "application/pdf"
                      }}
                    ]
                    If the text fragment does not contain transactions or cannot be processed, return an empty JSON list: `[]`.
                    Teks fragmen {i + 1}:
                    ```
                    {chunk}
                    ```
                    \"\"\"
                    try:
                        response = client.models.generate_content(
                            model=model_name,
                            contents=[prompt],
                        )
                        
                        try:
                            response_text = response.text.strip()
                            if response_text.startswith("```json"):
                                response_text = response_text[len("```json"):].strip()
                            if response_text.endswith("```"):
                                response_text = response_text[:-len("```")].strip()
                            chunk_extracted_data = json.loads(response_text)
                            log_message(f"    ✅ Successfully parsed JSON response for chunk {i + 1}.", log_file_absolute)
                            print(f"    ✅ Successfully parsed JSON response for chunk {i + 1}.")
                            all_extracted_data.extend(chunk_extracted_data) # Aggregate data
                        except json.JSONDecodeError as e:
                            log_message(f"   ❌ Failed to parse JSON response for chunk {i + 1} after cleaning: {e}. Full response text: {response.text}", log_file_absolute)
                            print(f"   ❌ Failed to parse JSON response for chunk {i + 1} after cleaning: {e}. Response text: {response.text[:500]}...")
                    except Exception as e:
                        log_message(f"   ❌ Gagal mengirim prompt ke Gemini untuk chunk {i + 1}: {e}", log_file_absolute)
                        print(f"   ❌ Gagal mengirim prompt ke Gemini untuk chunk {i + 1}: {e}")
            except Exception as e:
                log_message(f"❌ Failed to extract text from PDF file {file_name} using pdfminer.six: {e}", log_file_absolute)
                print(f"❌ Failed to extract text from PDF file {file_name} using pdfminer.six: {e}")
                return [] # Return empty list on PDF text extraction failure
        else: # Existing logic for non-PDF files
            log_message("Processing as non-PDF file.", log_file_absolute)
            print("Processing as non-PDF file.")
            uploaded_file = None # Initialize uploaded_file
            try:
              uploaded_file = client.files.upload(file=file_path)
              log_message(f"    File '{file_name}' berhasil diunggah ke Gemini.", log_file_absolute)
              print(f"    File '{file_name}' berhasil diunggah ke Gemini.")
            except Exception as e:
              log_message(f"   ❌ Gagal mengunggah file '{file_name}' ke Gemini: {e}", log_file_absolute)
              print(f"   ❌ Gagal mengunggah file '{file_name}' ke Gemini: {e}")
              return [] # Return empty list on upload failure
            prompt = f\"\"\"
            Anda adalah asisten keuangan ahli yang sangat teliti. Tugas Anda adalah mengekstrak SEMUA data transaksi dari file ini.
            Ikuti instruksi ini dengan SANGAT HATI-HATI:
            1.  Analisis file dan identifikasi setiap baris transaksi.
            2.  Untuk setiap transaksi yang ditemukan, ekstrak informasi berikut:
                * `transaksi_id`: Nomor transaksi jika ada, generate otomatis jika tidak ada.
                * `tanggal`: Format "YYYY-MM-DD".
                * `deskripsi`: Deskripsi lengkap dan jelas.
                * `kategori`: Saran kategori yang relevan.
                * `sub_kategori`: Saran sub-kategori yang lebih spesifik.
                * `tipe`: Harus "Pemasukan" atau "Pengeluaran".
                * `jumlah`: Angka saja.
                * `akun`: Gunakan nama file ({clean_file_name}).
                * `dari`: Pengirim dana.
                * `ke`: Tujuan/Penerima dana.
                * `jenisfile`: {mime_type}
            3.  Keluarkan hasilnya HANYA dalam format list JSON yang valid. JANGAN tambahkan teks lain.
            \"\"\"
            try:
                response = client.models.generate_content(
                    model=model_name,
                    contents=[uploaded_file, prompt],
                    )
                
                try:
                    response_text = response.text.strip()
                    if response_text.startswith("```json"):
                        response_text = response_text[len("```json"):].strip()
                    if response_text.endswith("```"):
                        response_text = response_text[:-len("```")].strip()
                    all_extracted_data = json.loads(response_text)
                    log_message(f"    ✅ Successfully parsed JSON response from Gemini.", log_file_absolute)
                    print(f"    ✅ Successfully parsed JSON response from Gemini.")
                except json.JSONDecodeError as e:
                    log_message(f"   ❌ Failed to parse JSON response from Gemini after cleaning: {e}. Full response text: {response.text}", log_file_absolute)
                    print(f"   ❌ Failed to parse JSON response from Gemini after cleaning: {e}. Response text: {response.text[:500]}...")
                    return []
            except Exception as e:
                log_message(f"   ❌ Gagal mengirim prompt ke Gemini: {e}", log_file_absolute)
                print(f"   ❌ Gagal mengirim prompt ke Gemini: {e}")
                return []
    except Exception as e:
        log_message(f"❌ Failed to process with Gemini for {file_name}: {e}", log_file_absolute)
        print(f"❌ Failed to process with Gemini: {e}")
        return []
    return all_extracted_data

def append_to_gsheet(data, worksheet_name, sheet_name, file_name):
    """Appends data to Google Sheet after checking for duplicates."""
    if not data:
        log_message("🟡 No data to append to Google Sheet.", log_file_absolute)
        print("🟡 No data to append to Google Sheet.")
        return
    try:
        gc = gspread.service_account(filename=gs_json)
        sh = gc.open(worksheet_name)
        worksheet = sh.worksheet(sheet_name)
        log_message("🧐 Retrieving existing data from Google Sheet...", log_file_absolute)
        print("🧐 Retrieving existing data from Google Sheet...")
        try:
          existing_data = worksheet.get_all_records()
        except Exception as e:
          log_message(f"❌ Failed to retrieve existing data from Google Sheet: {e}", log_file_absolute)
          print(f"❌ Failed to retrieve existing data from Google Sheet: {e}")
          existing_data = []
        column_compare = ['transaksi_id','tanggal', 'tipe', 'jumlah', 'akun']
        existing_df = pd.DataFrame(existing_data)
        if not existing_df.empty:
            existing_df = existing_df[column_compare]
        log_message(f"✅ Retrieved {len(existing_df)} existing rows.", log_file_absolute)
        print(f"✅ Retrieved {len(existing_df)} existing rows.")
        
        new_df = pd.DataFrame(data)
        log_message(f"✨ New data extracted from file: {len(new_df)} rows.", log_file_absolute)
        print(f"✨ New data extracted from file: {len(new_df)} rows.")
        print("Existing Data:")
        display(existing_df.head())
        print("New Data:")
        display(new_df.head())
        for col in column_compare:
            if col not in existing_df.columns:
                existing_df[col] = ''
            existing_df[col] = existing_df[col].astype(str).str.strip()
            
            if col not in new_df.columns:
                 new_df[col] = ''
            new_df[col] = new_df[col].astype(str).str.strip()
        log_message("🔬 Merging new data with existing data to check for duplicates...", log_file_absolute)
        print("🔬 Merging new data with existing data to check for duplicates...")
        merged_df = new_df.merge(existing_df, on=column_compare, how='left', indicator=True)
        unique_data = merged_df[merged_df['_merge'] == 'left_only'].drop(columns=['_merge']).to_dict('records')
        print("Unique Data:")
        display(merged_df[merged_df['_merge'] == 'left_only'].drop(columns=['_merge']))
        log_message(f"Found {len(unique_data)} unique rows to append.", log_file_absolute)
        print(f"Found {len(unique_data)} unique rows to append.")
        if not unique_data:
            log_message("🟡 No unique data to append after checking for duplicates.", log_file_absolute)
            print("🟡 No unique data to append after checking for duplicates.")
            return
        rows_to_append = []
        now_gmt7 = datetime.now(pytz.timezone('Asia/Jakarta')).strftime('%Y-%m-%d %H:%M:%S')
        for trx in unique_data:
            rows_to_append.append([
                trx.get('transaksi_id', ''), trx.get('tanggal', ''), trx.get('deskripsi', ''),
                trx.get('kategori', ''), trx.get('sub_kategori', ''), trx.get('tipe', ''),
                trx.get('jumlah', 0), trx.get('akun', ''), trx.get('dari', ''),
                trx.get('ke', ''), trx.get('jenisfile', ''), file_name, now_gmt7,
            ])
        worksheet.append_rows(rows_to_append, value_input_option='USER_ENTERED')
        log_message(f"✅ Successfully added {len(rows_to_append)} unique rows to '{sheet_name}'.", log_file_absolute)
        print(f"✅ Successfully added {len(rows_to_append)} unique rows to '{sheet_name}'.")
    except Exception as e:
        log_message(f"❌ Failed to add data to Google Sheet: {e}", log_file_absolute)
        print(f"❌ Failed to add data to Google Sheet: {e}")
def archive_file(full_file_path, archive_path):
    """Moves file to archive folder, including account name in filename."""
    try:
        os.makedirs(archive_path, exist_ok=True)
        now_gmt7 = datetime.now(pytz.timezone('Asia/Jakarta')).strftime('%Y%m%d_%H%M%S')
        clean_file_name = os.path.basename(full_file_path)
        shutil.move(full_file_path, os.path.join(archive_path, f"{now_gmt7}_{clean_file_name}"))
        log_message(f"✅ Successfully moved '{clean_file_name}' to archive folder.", log_file_absolute)
        print(f"✅ Successfully moved '{clean_file_name}' to archive folder.")
    except Exception as e:
        log_message(f"❌ Failed to archive file: {e}", log_file_absolute)
        print(f"❌ Failed to archive file: {e}")
def setup_environment():
    """Melakukan mounting Drive dan otentikasi."""
    try:
        drive.mount('/content/drive', force_remount=False)
        print("✅ Google Drive dan Otentikasi berhasil disiapkan.")
        return True
    except Exception as e:
        print(f"❌ Gagal melakukan setup: {e}")
        return False
def log_message(message, log_file):
    """Appends a timestamped message to the log file."""
    try:
        with open(log_file, 'a') as f:
            f.write(f"{datetime.now(pytz.timezone('Asia/Jakarta')).strftime('%Y-%m-%d %H:%M:%S')} - {message}\n")
    except Exception as e:
        print(f"❌ Failed to write to log file: {e}")
def get_drive_path(folder_path):
    """Mengubah path relatif menjadi path absolut di Google Drive."""
    return os.path.join('/content/drive/My Drive/', folder_path)
# --- Main Execution ---
print("--- STARTING PROCESS ---")
raw_data_path_abs = get_drive_path(raw_data_path)
archive_path_abs = get_drive_path(archive_path)
log_file_absolute = get_drive_path(log_file_path)
if setup_environment():
    log_dir = os.path.dirname(log_file_absolute)
    os.makedirs(log_dir, exist_ok=True)
    log_message("--- STARTING PROCESS ---", log_file_absolute)
    
    if not api_key or "MASUKKAN_API_KEY_ANDA_DISINI" in api_key or not sheet_name or not worksheet_name:
        log_message("❌ Harap isi semua parameter yang diperlukan (api_key, sheet_name, worksheet_name).", log_file_absolute)
        print("❌ Harap isi semua parameter yang diperlukan (api_key, sheet_name, worksheet_name).")
    elif not os.path.exists(raw_data_path_abs):
        log_message(f"❌ Folder data mentah tidak ditemukan di: {raw_data_path_abs}", log_file_absolute)
        print(f"❌ Folder data mentah tidak ditemukan di: {raw_data_path_abs}")
    else:
        files_to_process = [f for f in os.listdir(raw_data_path_abs) if os.path.isfile(os.path.join(raw_data_path_abs, f))]
        log_message(f"List files to process: {files_to_process}", log_file_absolute)
        print(f"List files to process: {files_to_process}")
        if not files_to_process:
            log_message("🟡 Tidak ada file untuk diproses di folder raw_data.", log_file_absolute)
            print("🟡 Tidak ada file untuk diproses di folder raw_data.")
        else:
            for file_name in files_to_process:
                full_file_path = os.path.join(raw_data_path_abs, file_name)
                log_message("\n" + "="*50, log_file_absolute)
                log_message(f"📄 Processing file: {file_name}", log_file_absolute)
                
                try:
                  extracted_data = process_file_with_gemini(full_file_path, api_key, model_name, log_file_absolute)
                  if extracted_data:
                      try:
                        append_to_gsheet(extracted_data, worksheet_name, sheet_name, file_name)
                        archive_file(full_file_path, archive_path_abs)
                      except Exception as e:
                        log_message(f"❌ Gagal memproses file setelah ekstraksi: {e}", log_file_absolute)
                        print(f"❌ Gagal memproses file setelah ekstraksi: {e}")
                  else:
                      log_message(f"⚠️ Melewati file '{file_name}' karena tidak ada data yang dapat diekstrak.", log_file_absolute)
                      print(f"⚠️ Melewati file '{file_name}' karena tidak ada data yang dapat diekstrak.")
                except Exception as e:
                  log_message(f"❌ Gagal memproses dengan Gemini: {e}", log_file_absolute)
                  print(f"❌ Gagal memproses dengan Gemini: {e}")
            log_message("\n" + "="*50, log_file_absolute)
            log_message("✅🎉 Semua file telah diproses! 🎉✅", log_file_absolute)
            print("\n" + "="*50)
            print("✅🎉 Semua file telah diproses! 🎉✅")

Berikut untuk tampilan dari hasil prosesnya di google colab:

Google Colab


Langkah 1: Persiapan Awal di Google Colab - Fondasi Otomatisasi Kalian

Google Colab akan menjadi tempat kita menjalankan skrip Python yang menjadi motor penggerak sistem ini. Kelebihannya, Colab tidak memerlukan konfigurasi rumit dan menyediakan akses ke sumber daya komputasi secara gratis.

Instalasi Pustaka yang Diperlukan

Pertama, kita perlu memastikan semua "alat bantu" atau pustaka Python yang dibutuhkan sudah terpasang di lingkungan Colab kita. Skrip yang akan kita gunakan memerlukan beberapa pustaka spesifik. Perintah `!pip install ...` di awal skrip akan menangani ini secara otomatis.

  • python-magic: Untuk mendeteksi tipe file.
  • pdfminer.six: Untuk bekerja dengan file PDF, terutama ekstraksi teks.
  • gspread: Untuk berinteraksi dengan Google Sheets.
  • google-generativeai: Untuk menggunakan Gemini API.
  • pandas: Untuk manipulasi data dalam format tabel (DataFrame).

Konfigurasi Parameter Skrip

Skrip Python yang akan kita gunakan memiliki beberapa parameter penting yang perlu kalian sesuaikan agar sistem berjalan sesuai dengan setup Google Drive dan Google Sheets kalian. Berikut adalah tabel parameter konfigurasi utama yang perlu kalian perhatikan:

Nama Parameter Deskripsi Contoh Isi dari Skrip Catatan/Yang Perlu Diubah Pengguna
model_name Model Gemini yang akan digunakan untuk ekstraksi dan kategorisasi. "gemini-1.5-flash" Pilih model Gemini sesuai kebutuhan dan ketersediaan. "gemini-1.5-flash" adalah pilihan yang baik untuk keseimbangan kecepatan dan kemampuan.
api_key Kunci API unik kalian untuk mengakses Google AI Studio (Gemini API). "MASUKKAN_API_KEY_ANDA_DISINI" WAJIB DIGANTI dengan API Key milik kalian. Dapatkan dari https://ai.google.dev/.
sheet_name Nama worksheet (tab) di dalam Google Spreadsheet tempat data transaksi akan disimpan. "Transaksi" Sesuaikan dengan nama worksheet yang kalian inginkan atau sudah ada.
worksheet_name Nama file Google Spreadsheet utama kalian. "Laporan Keuangan" Sesuaikan dengan nama file Spreadsheet kalian di Google Drive.
raw_data_path Path relatif di Google Drive kalian tempat menyimpan file e-statement/invoice yang akan diproses. "Learning/Laporan Keuangan/raw_data" Ganti dengan struktur folder yang kalian gunakan di Google Drive. Contoh: "Dokumen Keuangan/Untuk Diproses".
archive_path Path relatif di Google Drive kalian tempat menyimpan file yang sudah berhasil diproses. "Learning/Laporan Keuangan/archived" Ganti dengan struktur folder arsip yang kalian inginkan. Contoh: "Dokumen Keuangan/Sudah Diproses".
log_file_path Path relatif di Google Drive kalian tempat menyimpan file log proses. "Learning/Laporan Keuangan/processing.log" Ganti dengan lokasi file log yang kalian inginkan. Contoh: "Dokumen Keuangan/Logs/proses.log".
gs_json Path absolut di Google Colab menuju file kredensial JSON service account Google Cloud kalian. File ini diperlukan agar gspread bisa mengakses Google Sheets dan Google Drive. "/content/drive/My Drive/Learning/Laporan Keuangan/admin-bayhaqy.json" WAJIB DIGANTI. Upload file JSON service account kalian ke Google Drive, lalu sesuaikan path ini. Contoh: "/content/drive/My Drive/kredensial-gcp.json". Kalian perlu membuatnya di Google Cloud Console di bagian "IAM & Admin" > "Service Accounts".

Ketelitian dalam mengisi parameter-parameter ini sangat krusial. Kesalahan kecil dapat menyebabkan keseluruhan sistem otomatisasi ini tidak berjalan sebagaimana mestinya.

Menghubungkan Google Colab dengan Google Drive Kalian

Agar skrip di Colab bisa membaca file invoice dari Google Drive kalian, kalian perlu melakukan "mounting" Google Drive. Kode `drive.mount('/content/drive')` akan menangani ini. Setelah menjalankan sel, kalian akan diminta untuk melakukan otentikasi dan memberikan izin.

Langkah 2: Mesin Ekstraksi Data - Memanfaatkan Gemini API

Setelah semua persiapan selesai, saatnya kita membangun mesin utama yang akan bekerja mengekstrak informasi berharga dari dokumen keuangan kalian.

Proses Pengambilan dan Deteksi File

Skrip akan secara otomatis menjelajahi folder raw_data_path kalian. Dengan bantuan pustaka magic, skrip dapat mendeteksi tipe file (misalnya PDF, JPG, dll). Kemampuan ini penting karena perlakuan terhadap file PDF akan sedikit berbeda dibandingkan format file lainnya.

Untuk File PDF: Ekstraksi Teks dan Strategi Token

  • Ekstraksi Teks: Skrip akan memanfaatkan pustaka pdfminer.six untuk mengambil seluruh teks yang ada di dalam dokumen PDF.
  • Strategi Token: Model AI seperti Gemini memiliki batasan jumlah teks (token) yang bisa diproses. Untuk mengatasi ini, skrip akan memecah teks yang panjang menjadi potongan-potongan yang lebih kecil menggunakan textwrap. Setiap potongan ini kemudian akan diproses secara individual oleh Gemini.

Untuk File Non-PDF: Pendekatan Langsung

Jika file bukan PDF (misalnya, gambar struk), skrip akan mengunggah file tersebut secara langsung ke Gemini API, yang akan mencoba memahami dan mengekstrak informasi dari konten file tersebut.

Meracik Prompt yang Efektif untuk Gemini API

Kunci untuk mendapatkan hasil ekstraksi yang akurat adalah pada prompt yang kita berikan. Prompt yang baik harus sangat detail dan spesifik, mencakup:

  • Peran AI Ditetapkan: Meminta Gemini berperan sebagai "asisten keuangan ahli yang sangat teliti."
  • Instruksi Jelas: Memberikan arahan yang tegas, seperti "ekstrak SEMUA data transaksi" dan "pastikan format tanggal adalah YYYY-MM-DD".
  • Detail Kolom Ekstraksi: Merinci setiap kolom data yang harus diekstrak (tanggal, deskripsi, jumlah, kategori, dll.).
  • Format Output Ditegaskan: Menginstruksikan Gemini untuk mengeluarkan hasilnya HANYA dalam format list JSON yang valid.
  • Contoh Disediakan: Memberikan contoh format JSON yang diharapkan.
  • Penanganan Kasus Kosong: Meminta Gemini mengembalikan list JSON kosong (`[]`) jika tidak ada transaksi ditemukan.

Struktur Output JSON yang Dihasilkan

Gemini API akan mengembalikan data dalam format JSON. Memahami struktur ini penting untuk langkah selanjutnya. Berikut adalah detail kolom yang diharapkan:

Nama Field JSON Deskripsi Field Catatan Tambahan
transaksi_id Nomor unik transaksi. Digenerate otomatis jika tidak ada.
tanggal Tanggal transaksi. Format "YYYY-MM-DD".
deskripsi Deskripsi lengkap dan jelas dari transaksi. Bersihkan dan ubah menjadi huruf kapital.
kategori Kategori yang paling relevan. Contoh: Belanja Pribadi, Makanan & Minuman, Transportasi, dll.
sub_kategori Sub-kategori yang lebih spesifik. Contoh: Belanja Online, Makan Siang, Ojek Online, dll.
tipe Jenis transaksi. Harus "Pemasukan" atau "Pengeluaran".
jumlah Nominal transaksi (hanya angka). Tanpa titik, koma, atau "Rp".
akun Akun yang digunakan untuk transaksi. Gunakan nama file sebagai dasar penentuan akun.
dari Pengirim dana/sumber transaksi. Diekstrak dari info di deskripsi.
ke Penerima dana/tujuan transaksi. Diekstrak dari info di deskripsi.
jenisfile Tipe file sumber. Diisi dengan tipe MIME yang terdeteksi.

Langkah 3: Penyimpanan Cerdas - Mengelola Data di Google Sheets

Setelah data diekstrak, langkah selanjutnya adalah menyimpannya ke Google Sheets. Pustaka gspread dan pandas di Python menjadi pilihan efektif untuk tugas ini.

Mekanisme Pengecekan Duplikasi Data

Salah satu fitur paling cerdas dari sistem ini adalah kemampuannya mencegah entri data ganda. Skrip akan membandingkan data baru dengan data yang sudah ada di Google Sheet berdasarkan beberapa kolom kunci (seperti tanggal, tipe, jumlah, akun). Hanya transaksi yang benar-benar baru yang akan ditambahkan.

Menambahkan Transaksi Unik dan Mengarsipkan File

Setelah transaksi unik diidentifikasi, skrip akan menambahkannya ke Google Sheet, lengkap dengan nama file sumber dan timestamp pemrosesan. Untuk menjaga kerapian, file yang telah berhasil diproses kemudian akan dipindahkan ke folder arsip.

Langkah 4: Visualisasi Dinamis - Menghidupkan Data dengan Looker Studio

Data yang terkumpul di Google Sheets siap diubah menjadi insight. Di sinilah Looker Studio (sebelumnya Google Data Studio) berperan.

Google Looker Studio


Menghubungkan Google Sheet sebagai Sumber Data

  1. Masuk ke lookerstudio.google.com.
  2. Buat Sumber Data Baru (Data Source).
  3. Pilih konektor Google Sheets.
  4. Pilih Spreadsheet dan worksheet yang berisi data keuangan.
  5. Konfigurasi tipe data untuk setiap kolom (misalnya, Tanggal, Angka, Teks) dan klik Hubungkan.

Mengatur Data Freshness Otomatis Setiap 15 Menit

Agar dashboard selalu menampilkan informasi terbaru, atur interval refresh data. Di Looker Studio, Anda dapat mengatur "Data freshness" untuk sumber data Google Sheets ke "Every 15 minutes".

Membangun Dashboard Keuangan yang Interaktif

Rancang dashboard dengan berbagai komponen:

  • Ringkasan Pemasukan vs. Pengeluaran: Gunakan Scorecard.
  • Tren Transaksi Berdasarkan Waktu: Gunakan Grafik Garis.
  • Distribusi Pengeluaran berdasarkan Kategori: Gunakan Grafik Pie atau Treemap.
  • Daftar Transaksi Terbaru: Gunakan Tabel.
  • Filter Interaktif: Tambahkan kontrol filter berdasarkan Rentang Tanggal, Akun, atau Kategori.

Kesimpulan

Dengan menggabungkan kekuatan Google Colab, Gemini API, Google Sheets, dan Looker Studio, Anda dapat membangun sistem laporan keuangan otomatis yang efisien dan canggih menggunakan alat-alat gratis. Keunggulan Utama yang Anda Dapatkan:

  • Otomatisasi Penuh: Ucapkan selamat tinggal pada entri data manual.
  • Hemat Waktu dan Tenaga: Fokus pada analisis, bukan pengumpulan data.
  • Akurasi Data Lebih Tinggi: Mengurangi risiko kesalahan manusia.
  • Insight Keuangan Mendekati Real-Time: Pantau kondisi keuangan dengan data yang selalu segar.
  • GRATIS: Sebagian besar komponen utama dalam solusi ini dapat digunakan tanpa biaya.

Glosarium

API (Application Programming Interface): Antarmuka yang memungkinkan dua aplikasi berkomunikasi satu sama lain.
Google Colab: Layanan notebook Jupyter gratis berbasis cloud dari Google untuk menjalankan kode Python.
Gemini API: Model AI generatif dari Google yang digunakan untuk mengekstrak informasi dari dokumen.
JSON (JavaScript Object Notation): Format data berbasis teks yang ringan dan mudah diurai oleh mesin.
Looker Studio: Alat visualisasi data gratis dari Google untuk membuat dashboard interaktif.
pdfminer.six: Pustaka Python untuk mengekstrak teks dari dokumen PDF.
gspread: Pustaka Python yang mempermudah interaksi dengan Google Sheets API.
pandas: Pustaka Python fundamental untuk analisis dan manipulasi data.
Prompt Engineering: Proses merancang input (prompt) untuk model AI demi mendapatkan output yang diinginkan.
Data Freshness (Looker Studio): Pengaturan yang mengontrol seberapa sering data diperbarui dalam dashboard.