Lewati ke isi

14 β€” Desain: Kas, Biaya, dan Pajak

Status: DRAFT β€” belum final, menunggu evaluasi pemilik Dibaca oleh: Database Engineer, Backend Engineer Tujuan: mendokumentasikan keputusan arsitektur untuk modul kas, biaya, dan perpajakan β€” sebagai perbaikan dari model legacy yang selama 20 tahun menggabungkan fungsi-fungsi ini dalam satu tabel.


1. Temuan dari Legacy

1a. Tabel t (transaksi) β€” field keuangan utama

Field legacy Kolom baru Makna
t.nilai subtotal Sum baris detail saja (sebelum biaya, diskon, pembulatan)
t.biaya (β†’ transaksi_detail BIAYA) Biaya tambahan β€” akan pindah ke detail
t.diskon diskon Diskon keseluruhan nota
t.pembulatan pembulatan Pembulatan
t.nilaitotal total Sama persis β€” sudah termasuk semua komponen
t.bayar dibayar Uang muka / DP saat transaksi
t.rek_kas akun_kas_kode Akun kas yang menerima pembayaran
t.rek_kredit akun_kredit_kode Akun piutang/hutang
t.kredit kredit Piutang/hutang awal = total βˆ’ dibayar
t.pembayaran total_pelunasan Total pelunasan yang masuk setelahnya
t.potongnota potongnota Kredit dari retur yang dipotong ke nota ini
t.saldo (dihitung) kredit βˆ’ total_pelunasan βˆ’ potongnota

Rumus total (berlaku di legacy dan skema baru):

total = (subtotal + biaya + pembulatan) βˆ’ diskon
      = (t.nilai  + t.biaya + t.pembulatan) βˆ’ t.diskon
      = t.nilaitotal   ← legacy sudah menghitung ini

Catatan penting untuk ETL (V003): - total β†’ t.nilaitotal ← sudah benar di V003 - subtotal β†’ t.nilai ← salah di V003, saat ini memakai t.nilaitotal

1b. Tabel kas legacy β€” dua peran berbeda dalam satu tabel

Query ke database (2026-05-20) menunjukkan bahwa kas.tipe membedakan dua fungsi yang sangat berbeda:

kas.tipe Dipakai kdtrans Fungsi sebenarnya
KAS KK, KM Buku kas β€” detail tiap baris pengeluaran/penerimaan
KREDIT PP, PH, OP, OH Ledger AR/AP β€” faktur mana yang dilunasi

kdtrans yang punya kas:

kdtrans Jumlah transaksi Jumlah kas rows Keterangan
PP 89.411 89.398 Pelunasan piutang
KK 39.091 39.013 Kas keluar (pengeluaran)
PH 11.156 11.154 Pelunasan hutang
KM 1.538 1.533 Kas masuk (setoran)
OP 246 246 Opening piutang
OH 37 37 Opening hutang

kdtrans tanpa kas (piutang/hutang disimpan di kolom t.kredit/t.saldo): PJ, PI, GP, PB, DA, PI, PL, RJ, RB, HU, DA, JU, OS

1c. Pola kas KK/KM (Buku Kas)

KK-25851 | dari: 110.11 Kas Pusat | total=45.000
  kas row 1: rek=613.00 | uraian="3 Koli Dari Jatayu Sakti"
  kas row 2: rek=613.00 | uraian="Ongkos Bongkar"
  (field debit/kredit sering NULL β€” hanya rek + uraian yang terisi)

Jurnal yang dihasilkan:
  D 613.00 Biaya Angkut    45.000
  K 110.11 Kas Pusat       45.000

1d. Pola kas PP/PH (Ledger AR/AP)

PP-65946 | rekkas=110.27 Bank | total=487.600
  kas row 1: rek=130.00 | rekkas=110.27 | refidtrans=PJ-200677 | ket="Bayar PJ-200677"
  kas row 2: rek=130.00 | rekkas=110.27 | refidtrans=PJ-202025 | ket="Bayar PJ-202025"
  (1 PP bisa melunasi banyak faktur PJ sekaligus)

Jurnal yang dihasilkan:
  D 110.27 Bank BNI         487.600
  K 130.00 Piutang PJ-200677 250.000
  K 130.00 Piutang PJ-202025 237.600

2. Masalah di Model Legacy

  1. Satu tabel dua fungsi β€” kas dipakai untuk buku kas sekaligus ledger AR/AP. Query harus selalu filter kas.tipe untuk tidak salah baca.

  2. Biaya tidak ada detailnya β€” t.biaya hanya satu angka di header. Tidak bisa tahu berapa biaya kirim vs biaya packing.

  3. Uang muka hanya satu akun β€” t.bayar + t.rek_kas hanya bisa satu rekening. Tidak bisa DP cash + transfer dalam satu nota.

  4. Saldo disimpan, bukan dihitung β€” t.saldo bisa tidak sinkron jika ada bug di prosedur update.

  5. Tidak ada perpajakan β€” PPN, PPh tidak tercatat sama sekali.


3. Keputusan Arsitektur Baru

Prinsip: perbaikan, bukan penggantian β€” pola yang sudah familiar dipertahankan, kelemahan struktural diperbaiki.

3a. KK/KM β†’ masuk ke transaksi_detail (tipe_baris = BIAYA)

Bukti Kas Keluar (BKK) dan Bukti Kas Masuk (BKM) menjadi transaksi biasa. Detail tiap baris pengeluaran/penerimaan masuk ke transaksi_detail dengan tipe_baris = 'BIAYA' dan akun_kode diisi (bukan barang_id).

Transaksi KK-001 | jenis_kode=KK | akun_kas_kode=110.11 | total=49.000
  transaksi_detail urut=1:
    tipe_baris=BIAYA | akun_kode=613.00 | qty=1 | harga=45.000 | ket="Ongkos Bongkar"
  transaksi_detail urut=2:
    tipe_baris=BIAYA | akun_kode=631.03 | qty=1 | harga=4.000  | ket="Spons Cuci Piring"

Trigger menghasilkan jurnal otomatis:

D 613.00 Biaya Angkut       45.000
D 631.03 Perlengkapan Kantor  4.000
K 110.11 Kas Pusat           49.000

3b. PP/PH β†’ masuk ke transaksi_detail (tipe_baris = PELUNASAN)

Transaksi pelunasan piutang/hutang menjadi transaksi biasa. Tiap faktur yang dilunasi menjadi satu baris transaksi_detail dengan ref_transaksi_id menunjuk ke transaksi asal (PJ / PI).

Transaksi PP-001 | jenis_kode=PP | akun_kas_kode=110.27 | total=487.600
  transaksi_detail urut=1:
    tipe_baris=PELUNASAN | akun_kode=130.00 | jumlah=250.000
    ref_transaksi_id β†’ PJ-200677.id | ket="Bayar PJ-200677"
  transaksi_detail urut=2:
    tipe_baris=PELUNASAN | akun_kode=130.00 | jumlah=237.600
    ref_transaksi_id β†’ PJ-202025.id | ket="Bayar PJ-202025"

Trigger saat insert PELUNASAN β†’ update total_pelunasan di transaksi asal:

UPDATE transaksi
SET total_pelunasan = total_pelunasan + NEW.jumlah
WHERE id = NEW.ref_transaksi_id;

3c. Saldo AR/AP tetap di header transaksi

Kolom kredit dan total_pelunasan di transaksi dipertahankan sebagai sumber kebenaran yang di-maintain trigger. Saldo bisa langsung di-query tanpa JOIN:

-- Daftar piutang belum lunas (pengganti t.saldo > 0)
SELECT * FROM transaksi
WHERE jenis_kode = 'PJ'
  AND kredit > total_pelunasan;

3d. Uang muka β†’ tabel transaksi_bayar βœ… DIPUTUSKAN

Uang muka dicatat di tabel terpisah transaksi_bayar. Mendukung multi-akun (cash + transfer = beberapa baris untuk satu transaksi).

Transaksi PJ-001 | total=1.000.000 | kredit=500.000

transaksi_bayar:
  transaksi_id=PJ-001 | akun_kode=110.01 Kas Kecil | jumlah=300.000 | ket="Tunai"
  transaksi_id=PJ-001 | akun_kode=120.07 Bank BNI  | jumlah=200.000 | ket="Transfer"

Trigger: transaksi.dibayar = SUM(transaksi_bayar.jumlah) WHERE transaksi_id = this.id transaksi.kredit = total βˆ’ dibayar

3e. Biaya ke pelanggan β†’ transaksi_detail (tipe_baris = BIAYA)

3f. Potongnota / Retur β€” logika dari legacy

potongnota di header transaksi dihitung otomatis dari detail retur yang mengarah ke transaksi ini dan menandai potong_nota = true:

-- Legacy:
new.potongnota = SUM(d.total)
  FROM d (transaksi_detail retur)
  WHERE d.refidtrans = new.id
    AND d.potong_nota = true

-- Saldo akhir:
saldo = kredit - pembayaran - potongnota

Di sistem baru: trigger pada transaksi_detail dengan tipe_baris='BARANG' dan ref_transaksi_id terisi + kolom is_potong_nota = true β†’ update transaksi.potongnota di transaksi asal.

Kolom yang perlu ditambah di transaksi_detail:

is_potong_nota  BOOLEAN DEFAULT false  -- khusus untuk baris retur yang memotong nota

Kolom yang perlu ditambah di transaksi:

potongnota  numeric(18,2) DEFAULT 0  -- di-maintain trigger dari RJ/RB

3g. GP β€” Sistem Poin Per Item βœ… DIKLARIFIKASI

GP bukan "Giro Pegawai" dalam konteks ini. Kode ini bertahan dari 20 tahun lalu dan sekarang dipakai untuk pembayaran poin ke sales/agen di akhir bulan.

Mekanisme: - Setiap transaksi_detail punya kolom poin β€” poin yang berhak diterima sales/agen jika nota tersebut lunas. - Saat PJ lunas (trigger PP/pelunasan) β†’ poin di detail "terbuka" / eligible. - Di akhir bulan, GP dibuat untuk mencatat pembayaran poin yang sudah eligible. - GP tanpa kas rows karena ini bukan AR/AP β€” langsung ke jurnal.

Query monitoring poin:

-- Poin eligible (nota sudah lunas, belum dibayar via GP)
SELECT td.*, t.nomor_transaksi
FROM transaksi_detail td
JOIN transaksi t ON t.id = td.transaksi_id
WHERE t.jenis_kode = 'PJ'
  AND t.kredit <= t.total_pelunasan  -- sudah lunas
  AND td.poin > 0
  AND NOT EXISTS (
    SELECT 1 FROM transaksi_detail gp
    WHERE gp.ref_transaksi_id = t.id
      AND gp.tipe_baris = 'POIN'
  );

Di sistem baru: GP tetap sebagai transaksi dengan transaksi_detail bertipe POIN, masing-masing baris mereferensi ref_transaksi_id ke PJ asal.

Biaya kirim, biaya packing yang ditagih ke pelanggan masuk sebagai baris BIAYA di transaksi_detail PJ yang sama β€” bukan di header.

Transaksi PJ-002 | total=1.050.000
  transaksi_detail:
    tipe_baris=BARANG | barang_id=... | jumlah=1.000.000
    tipe_baris=BIAYA  | akun_kode=pendapatan_kirim | jumlah=50.000 | ket="Ongkir JNE"

4. Perubahan Schema

4a. transaksi_detail β€” tambah 2 kolom

ALTER TABLE transaksi_detail
  ADD COLUMN akun_kode     TEXT,
  ADD COLUMN tipe_baris    TEXT,
  ADD COLUMN is_potong_nota BOOLEAN DEFAULT false;

-- tipe_baris: BARANG | BIAYA | PELUNASAN | PAJAK | DISKON | POIN
-- akun_kode: diisi untuk baris non-barang
-- barang_id: diisi untuk BARANG, NULL untuk yang lain
-- is_potong_nota: true hanya pada detail RJ/RB yang memotong saldo nota asal

4b. transaksi β€” kolom yang berubah / ditambah

Kolom lama Kolom baru Sumber update Catatan
t.nilai subtotal input Sum baris detail saja
t.nilaitotal total input (subtotal+biaya+pembulatan)βˆ’diskon
t.diskon diskon input β€”
t.pembulatan pembulatan input β€”
t.bayar dibayar trigger dari transaksi_bayar Sum uang muka
t.kredit kredit trigger total βˆ’ dibayar
t.pembayaran total_pelunasan trigger dari PP/PH detail Sum pelunasan masuk
t.potongnota potongnota trigger dari RJ/RB detail Sum retur potong nota
t.saldo (dihitung) β€” kredit βˆ’ total_pelunasan βˆ’ potongnota

4c. transaksi_bayar β€” tabel baru βœ… DIPUTUSKAN

CREATE TABLE transaksi_bayar (
  id            BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  transaksi_id  BIGINT NOT NULL REFERENCES transaksi(id) ON DELETE CASCADE,
  akun_kode     TEXT   NOT NULL REFERENCES akun(kode),
  jumlah        numeric(18,2) NOT NULL CHECK (jumlah > 0),
  keterangan    TEXT,
  created_at    timestamptz DEFAULT now()
);
-- Trigger: AFTER INSERT/UPDATE/DELETE ON transaksi_bayar
--   β†’ UPDATE transaksi SET dibayar = SUM(jumlah) WHERE id = transaksi_id

4d. transaksi_pajak β€” tabel baru (⏳ DITUNDA)

Status: placeholder. Pemilik belum memiliki kebutuhan yang cukup jelas untuk PPN/PPh. Tabel ini disiapkan sebagai fondasi; isi dan trigger-nya ditentukan setelah ada diskusi lebih lanjut tentang alur perpajakan.

-- Placeholder β€” kolom bisa bertambah setelah spesifikasi pajak jelas
CREATE TABLE transaksi_pajak (
  id            BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  transaksi_id  BIGINT NOT NULL REFERENCES transaksi(id),
  tipe          TEXT,           -- PPN_KELUARAN | PPN_MASUKAN | PPH_23 | PPH_4_2 | ...
  tarif         numeric(5,2),
  dpp           numeric(18,2),  -- dasar pengenaan pajak
  jumlah        numeric(18,2),
  akun_kode     TEXT,
  created_at    timestamptz DEFAULT now()
);
-- Kolom e-Faktur/Coretax (npwp_lawan, nomor_seri_faktur, dll) ditambah nanti

5. Alur Lengkap per Tipe Transaksi

kdtrans Header Detail (tipe_baris) Transaksi Bayar Trigger efek pada
PJ kredit, dibayar, total BARANG, BIAYA βœ“ (uang muka) jurnal, stok, fifo, dibayar
PI kredit, dibayar, total BARANG, BIAYA βœ“ (uang muka) jurnal, stok, fifo, dibayar
KK akun_kas_kode, total BIAYA (akun, jumlah) β€” jurnal
KM akun_kas_kode, total BIAYA (akun, jumlah) β€” jurnal
PP akun_kas_kode, total PELUNASAN (ref, jumlah) β€” jurnal + PJ.total_pelunasan
PH akun_kas_kode, total PELUNASAN (ref, jumlah) β€” jurnal + PI.total_pelunasan
RJ ref_transaksi_id BARANG negatif + is_potong_nota β€” jurnal, restock, PJ.potongnota
RB ref_transaksi_id BARANG negatif + is_potong_nota β€” jurnal, restock, PI.potongnota
GP β€” POIN (ref ke PJ, jumlah poin) β€” jurnal poin + tandai poin terbayar

6. Yang Belum Diputuskan

Item Status Catatan
transaksi_bayar terpisah βœ… DIPUTUSKAN Tabel terpisah, multi-akun
potongnota βœ… DIKLARIFIKASI Trigger dari RJ/RB detail is_potong_nota=true
GP = sistem poin βœ… DIKLARIFIKASI Bukan giro, tipe_baris=POIN, bayar poin akhir bulan
t.biaya β†’ tipe_baris=BIAYA βœ… DIPUTUSKAN Pakai transaksi_detail
Perpajakan (PPN/PPh) ⏳ DITUNDA Pemilik belum cukup familiar β€” placeholder tabel sudah ada
Fix subtotal di V003 βœ… SELESAI t.nilai β†’ subtotal; t.nilaitotal β†’ total
ETL KK/KM/PP/PH historis βœ… DIPUTUSKAN History dan jurnal adalah dua hal berbeda β€” keduanya wajib di-ETL

7. Dampak ke ETL (V003)

Bug yang perlu diperbaiki di V003:

-- V003 baris 59: subtotal salah, memakai nilaitotal padahal seharusnya nilai
-- Saat ini:
COALESCE(t.nilaitotal, 0),   -- subtotal (SALAH β€” ini total, bukan subtotal)

-- Seharusnya:
COALESCE(t.nilai, 0),        -- subtotal = sum baris detail saja

-- V003 baris 63: total sudah BENAR
COALESCE(t.nilaitotal, 0),   -- total = (subtotal+biaya+pembulatan)βˆ’diskon βœ“
-- (nilaitotal di legacy sudah = formula lengkap)

Perlu konfirmasi: kolom t.nilai ada di stg_transaksi? Cek sebelum apply fix.

ETL tambahan yang diperlukan (belum dibuat):

Prinsip: History dan jurnal adalah dua hal berbeda. Jurnal (ayat akuntansi) sudah termuat. History (siapa bayar apa, nota mana yang dilunasi) perlu ETL terpisah agar lengkap. Script ETL harus aman dijalankan ratusan kali (idempoten).

  1. V005 β€” ETL KK/KM β†’ transaksi + transaksi_detail (tipe_baris=BIAYA)
  2. Sumber: stg_kas tipe KAS join stg_transaksi kdtrans IN ('KK','KM')
  3. Setiap kas row β†’ 1 transaksi_detail dengan akun_kode = kas.rek

  4. V006 β€” ETL PP/PH β†’ transaksi + transaksi_detail (tipe_baris=PELUNASAN)

  5. Sumber: stg_kas tipe KREDIT join stg_transaksi kdtrans IN ('PP','PH')
  6. Setiap kas row β†’ 1 transaksi_detail dengan ref_transaksi_id = transaksi asal

  7. V007 β€” ETL OP/OH β†’ saldo awal piutang/hutang di kas

  8. Sumber: stg_kas tipe KREDIT join stg_transaksi kdtrans IN ('OP','OH')

  9. DDL β€” tambah kolom akun_kode + tipe_baris + is_potong_nota ke transaksi_detail

  10. DDL β€” buat tabel transaksi_bayar

8. Catatan Evaluasi Pemilik

(kosongkan bagian ini untuk diisi saat review)

Sudah diputuskan: - [x] transaksi_bayar β†’ tabel terpisah, multi-akun - [x] KK/KM β†’ transaksi_detail dengan tipe_baris=BIAYA - [x] PP/PH β†’ transaksi_detail dengan tipe_baris=PELUNASAN + ref_transaksi_id - [x] potongnota β†’ trigger dari is_potong_nota=true di detail RJ/RB - [x] GP β†’ sistem poin per item, dibayar akhir bulan via transaksi GP

Masih perlu keputusan: - [x] Fix subtotal di V003: t.nilai β†’ subtotal βœ… - [x] ETL historis KK/KM/PP/PH: history dan jurnal berbeda β€” keduanya di-ETL βœ… - [x] Perpajakan: disiapkan sekarang β€” pusat.pajak + transaksi_pajak sudah dibuat βœ… - [x] saldo: GENERATED ALWAYS AS (kredit - total_pelunasan - potongnota) STORED βœ…


Dibuat: 2026-05-20 β€” berdasarkan sesi analisis data legacy u1566482_sparepart File: docs/14-DESAIN-KAS-BIAYA-PAJAK.md