AI Skill Report Card

Building Django Bookkeeping System

B74·Oct 1, 2026·Source: Web
13 / 15
Bash
python -m venv venv && source venv/bin/activate pip install django pandas openpyxl weasyprint django-admin startproject labkeuangan . python manage.py startapp pembukuan # tambahkan 'pembukuan' ke INSTALLED_APPS di labkeuangan/settings.py

Prinsip inti: satu tabel ledger (Transaksi) sebagai sumber kebenaran tunggal untuk semua arus uang (kredit/debit). Semua modul lain (stok, komisi, laporan) hanya membaca atau memicu baris baru di ledger ini — jangan buat tabel saldo terpisah yang bisa desinkron.

Recommendation▾
Content is cut off mid-code block in section 6 (ekspor Excel) - complete the PDF export section and ensure the skill file is not truncated
13 / 15
Progress:
- [ ] 1. Inisialisasi proyek & environment
- [ ] 2. Model database (Barang, Teknisi, KategoriTransaksi, Transaksi)
- [ ] 3. Otomasi logika transaksi (signals/save override)
- [ ] 4. Views & dashboard (CBV)
- [ ] 5. Skrip impor Excel data lama
- [ ] 6. Ekspor Excel & cetak PDF invoice/laporan

1. Inisialisasi Proyek

  • Buat virtual environment, install django, pandas, openpyxl, weasyprint.
  • django-admin startproject untuk root project, startapp pembukuan untuk logika bisnis.
  • Daftarkan app di INSTALLED_APPS, set TIME_ZONE = 'Asia/Jakarta', USE_TZ = True.
  • Gunakan SQLite untuk mulai (cukup untuk skala lab kecil); jangan over-engineer dengan Postgres kecuali diminta.

2. Model Database

Struktur minimal yang disarankan (pembukuan/models.py):

Python
from django.db import models from django.conf import settings class KategoriTransaksi(models.Model): KREDIT = 'KREDIT' # uang masuk DEBIT = 'DEBIT' # uang keluar TIPE_CHOICES = [(KREDIT, 'Kredit (Masuk)'), (DEBIT, 'Debit (Keluar)')] nama = models.CharField(max_length=100) # "Penjualan Barang", "Jasa Servis", "Beli Stok", "Gaji Teknisi" tipe = models.CharField(max_length=6, choices=TIPE_CHOICES) def __str__(self): return f"{self.nama} ({self.tipe})" class Barang(models.Model): nama = models.CharField(max_length=150) stok = models.PositiveIntegerField(default=0) harga_beli = models.DecimalField(max_digits=12, decimal_places=2) harga_jual = models.DecimalField(max_digits=12, decimal_places=2) def __str__(self): return self.nama class Teknisi(models.Model): nama = models.CharField(max_length=150) persen_komisi = models.DecimalField(max_digits=5, decimal_places=2, default=20.00) # 20% saldo_komisi_belum_cair = models.DecimalField(max_digits=12, decimal_places=2, default=0) def __str__(self): return self.nama class Transaksi(models.Model): tanggal = models.DateField(auto_now_add=False) kategori = models.ForeignKey(KategoriTransaksi, on_delete=models.PROTECT) keterangan = models.CharField(max_length=255) jumlah = models.DecimalField(max_digits=12, decimal_places=2) # selalu positif, arah ditentukan kategori.tipe # relasi opsional tergantung jenis transaksi barang = models.ForeignKey(Barang, null=True, blank=True, on_delete=models.SET_NULL) qty = models.PositiveIntegerField(null=True, blank=True) teknisi = models.ForeignKey(Teknisi, null=True, blank=True, on_delete=models.SET_NULL) dibuat_pada = models.DateTimeField(auto_now_add=True) dicatat_oleh = models.ForeignKey(settings.AUTH_USER_MODEL, null=True, on_delete=models.SET_NULL) class Meta: ordering = ['-tanggal', '-dibuat_pada'] def __str__(self): return f"{self.tanggal} - {self.keterangan} - {self.jumlah}"

Keputusan desain penting:

  • jumlah selalu disimpan positif; arah kredit/debit ditentukan oleh kategori.tipe, bukan tanda minus. Ini mencegah bug tanda ganda saat kalkulasi saldo.
  • Saldo kas tidak disimpan sebagai field — selalu dihitung on-the-fly dengan aggregate(Sum(...)) dari ledger, agar tidak pernah desinkron.

3. Otomasi Logika (gunakan save() override, bukan signals, untuk kasus sederhana)

Rekomendasi: override save() pada Transaksi, bukan post_save signal — lebih mudah dibaca & di-maintain untuk tim kecil, dan menghindari efek samping tersembunyi.

Python
from django.db import transaction as db_transaction class Transaksi(models.Model): # ...field di atas... def save(self, *args, **kwargs): is_new = self._state.adding with db_transaction.atomic(): super().save(*args, **kwargs) if is_new: self._proses_efek_samping() def _proses_efek_samping(self): nama_kategori = self.kategori.nama.lower() # Jual barang -> kurangi stok if 'jual' in nama_kategori and self.barang and self.qty: Barang.objects.filter(pk=self.barang.pk).update( stok=models.F('stok') - self.qty ) # Beli stok -> tambah stok elif 'beli stok' in nama_kategori and self.barang and self.qty: Barang.objects.filter(pk=self.barang.pk).update( stok=models.F('stok') + self.qty ) # Jasa servis -> hitung komisi teknisi elif 'servis' in nama_kategori and self.teknisi: komisi = self.jumlah * (self.teknisi.persen_komisi / 100) Teknisi.objects.filter(pk=self.teknisi.pk).update( saldo_komisi_belum_cair=models.F('saldo_komisi_belum_cair') + komisi ) # Pencairan gaji teknisi -> kurangi saldo komisi elif 'gaji' in nama_kategori and self.teknisi: Teknisi.objects.filter(pk=self.teknisi.pk).update( saldo_komisi_belum_cair=models.F('saldo_komisi_belum_cair') - self.jumlah )

Gunakan F() expressions + .update() agar atomic di level database (hindari race condition dibanding read-modify-write di Python).

4. Views & Dashboard

  • DashboardView(TemplateView): tampilkan saldo kas total (Sum kredit - Sum debit), stok menipis, saldo komisi teknisi belum cair.
  • TransaksiCreateView(CreateView): form input harian, dengan JS sederhana untuk sembunyikan/tampilkan field barang/teknisi sesuai kategori dipilih.
  • TransaksiListView(ListView): riwayat dengan filter tanggal & kategori lewat query params (?dari=&sampai=&kategori=).
  • PencairanGajiView(FormView): form khusus pilih teknisi → otomatis buat Transaksi kategori "Gaji Teknisi" sejumlah saldo komisi (atau sebagian).

Contoh perhitungan saldo kas:

Python
from django.db.models import Sum, Case, When, F, DecimalField def hitung_saldo_kas(): agg = Transaksi.objects.aggregate( total=Sum(Case( When(kategori__tipe='KREDIT', then=F('jumlah')), When(kategori__tipe='DEBIT', then=-F('jumlah')), output_field=DecimalField() )) ) return agg['total'] or 0

5. Impor Excel Data Lama

Buat sebagai management command (pembukuan/management/commands/impor_excel.py), bukan view — lebih aman untuk operasi sekali jalan dan bisa di-dry-run.

Python
import pandas as pd from django.core.management.base import BaseCommand from pembukuan.models import Transaksi, KategoriTransaksi class Command(BaseCommand): help = "Impor data pembukuan lama dari file Excel" def add_arguments(self, parser): parser.add_argument('filepath', type=str) parser.add_argument('--dry-run', action='store_true') def handle(self, *args, **options): df = pd.read_excel(options['filepath'], sheet_name=0) df = df.dropna(subset=['tanggal', 'kategori', 'jumlah']) berhasil, gagal = 0, 0 for _, row in df.iterrows(): try: kategori, _ = KategoriTransaksi.objects.get_or_create( nama=str(row['kategori']).strip(), defaults={'tipe': 'KREDIT' if row['jumlah'] >= 0 else 'DEBIT'} ) if not options['dry_run']: Transaksi.objects.create( tanggal=pd.to_datetime(row['tanggal']).date(), kategori=kategori, keterangan=str(row.get('keterangan', '')), jumlah=abs(row['jumlah']), ) berhasil += 1 except Exception as e: gagal += 1 self.stderr.write(f"Baris {_}: {e}") self.stdout.write(self.style.SUCCESS(f"Berhasil: {berhasil}, Gagal: {gagal}"))

Jalankan: python manage.py impor_excel data_lama.xlsx --dry-run dulu untuk validasi sebelum commit sungguhan.

6. Ekspor Excel & PDF

Ekspor Excel (view function, bukan CBV — lebih simpel untuk kasus I/O file):

Python
from openpyxl import Workbook from django.http import HttpResponse def ekspor_transaksi_excel(request): wb = Workbook() ws = wb.active ws.title = "Rekapitulasi" ws.append(['Tanggal', 'Kategori', 'Tipe', 'Keterangan', 'Jumlah']) for t in Transaksi.objects.select_related('kategori
Recommendation▾
Description is in Indonesian while criteria often expect clear trigger language; ensure it still clearly states 'what' and 'when' (it does, but could be tightened)
0
Grade BAI Skill Framework
Scorecard
Criteria Breakdown
Quick Start
13/15
Workflow
13/15
Examples
15/20
Completeness
13/20
Format
13/15
Conciseness
12/15