Lompat ke konten
dokumentasi.id
Commit 32b2830
v1
Star0

Kamu membaca versi lama. Halaman ini hanya bisa dibaca.

4

Model Domain dan Constraint Database

Model domain Hydro tersebar di empat app Django — produk, stok, pembelian, dan akun — ditambah app inti yang menyimpan tabel penomoran dokumen. Basis datanya PostgreSQL lewat django.db.backends.postgresql backend/config/settings.py:77-79. Setiap model menulis db_table eksplisit di Meta, sehingga nama tabel fisik sama persis dengan yang tertulis di kode, bukan nama default Django.

Peta tabel per app

AppTabel fisikModelDefinisi
produkkategoriKategoribackend/produk/models.py:9-18
produkprodukProdukbackend/produk/models.py:26-43
produksatuan_produkSatuanProdukbackend/produk/models.py:56-70
produkharga_produkHargaProdukbackend/produk/models.py:76-91
stokbarang_masukBarangMasukbackend/stok/models.py:8-26
stokbatch_produkBatchProdukbackend/stok/models.py:34-51
stokmutasi_stokMutasiStokbackend/stok/models.py:67-79
stokstok_opnameStokOpnamebackend/stok/models.py:82-92
stokstok_opname_itemStokOpnameItembackend/stok/models.py:115-126
pembelianpelangganPelangganbackend/pembelian/models.py:12-25
pembeliantransaksiTransaksibackend/pembelian/models.py:40-89
pembelianitem_transaksiItemTransaksibackend/pembelian/models.py:100-117
pembelianitem_transaksi_batchItemTransaksiBatchbackend/pembelian/models.py:120-135
pembelianpembayaranPembayaranbackend/pembelian/models.py:143-158
pembelianreturReturbackend/pembelian/models.py:164-176
pembelianretur_itemReturItembackend/pembelian/models.py:187-196
akunperanPeranbackend/akun/models.py:8-17
akunpenggunaPenggunabackend/akun/models.py:20-42
intinomor_urutNomorUrutbackend/inti/models.py:4-18

Peran memakai kode sebagai primary key, bukan id otomatis backend/akun/models.py:8-17. Tiga baris peran awal diisi lewat migrasi data RunPython backend/akun/migrations/0002_peran_awal.py:3-13, sedangkan Pengguna memperluas AbstractUser dengan kolom nama dan FK peran ber-on_delete=PROTECT backend/akun/models.py:20-42. Detail izin dan 2FA ada di Autentikasi, 2FA, dan Izin Peran.

Relasi antar entitas

erDiagram
  KATEGORI {
    bigint id PK
    varchar80 nama
    boolean aktif
  }
  PRODUK {
    bigint id PK
    varchar40 sku
    varchar160 nama
    bigint kategori_id FK
    integer ukuran_ml
    integer stok_minimum
    boolean aktif
  }
  SATUAN_PRODUK {
    bigint id PK
    bigint produk_id FK
    varchar30 nama
    integer isi_botol
  }
  HARGA_PRODUK {
    bigint id PK
    bigint satuan_id FK
    decimal18_2 harga
    date berlaku_mulai
  }
  BATCH_PRODUK {
    bigint id PK
    bigint produk_id FK
    bigint barang_masuk_id FK
    varchar60 nomor_batch
    date kedaluwarsa
    integer sisa
  }
  TRANSAKSI {
    bigint id PK
    varchar30 nomor
    uuid kunci_idempoten
    varchar16 status
    decimal18_2 total
    decimal18_2 total_retur
    decimal18_2 terbayar
    decimal18_2 sisa_tagihan
  }
  ITEM_TRANSAKSI_BATCH {
    bigint id PK
    bigint item_id FK
    bigint batch_id FK
    integer jumlah_botol
    integer jumlah_diretur
  }
  KATEGORI ||--o{ PRODUK : "mengelompokkan"
  PRODUK ||--o{ SATUAN_PRODUK : "punya satuan"
  SATUAN_PRODUK ||--o{ HARGA_PRODUK : "riwayat harga"
  PRODUK ||--o{ BARANG_MASUK : "diterima"
  BARANG_MASUK ||--|| BATCH_PRODUK : "menghasilkan satu batch"
  PRODUK ||--o{ BATCH_PRODUK : "dipilah"
  BATCH_PRODUK ||--o{ MUTASI_STOK : "dicatat per batch"
  STOK_OPNAME ||--o{ STOK_OPNAME_ITEM : "berisi"
  BATCH_PRODUK ||--o{ STOK_OPNAME_ITEM : "dihitung"
  PELANGGAN ||--o{ TRANSAKSI : "melakukan"
  TRANSAKSI ||--o{ ITEM_TRANSAKSI : "berisi"
  PRODUK ||--o{ ITEM_TRANSAKSI : "dijual"
  ITEM_TRANSAKSI ||--o{ ITEM_TRANSAKSI_BATCH : "dialokasikan"
  BATCH_PRODUK ||--o{ ITEM_TRANSAKSI_BATCH : "dipakai"
  TRANSAKSI ||--o{ RETUR : "diretur"
  RETUR ||--o{ RETUR_ITEM : "berisi"
  ITEM_TRANSAKSI_BATCH ||--o{ RETUR_ITEM : "diretur"
  TRANSAKSI ||--o{ PEMBAYARAN : "dibayar"
  TRANSAKSI ||..o{ MUTASI_STOK : "lewat referensi"
%% fig: Relasi tabel domain Hydro
%% desc: Enam belas tabel terhubung lewat foreign key PROTECT; satu relasi, Transaksi ke MutasiStok, hanya berupa kolom referensi tanpa foreign key.
Fig. 1: Relasi tabel domain HydroEnam belas tabel terhubung lewat foreign key PROTECT; satu relasi, Transaksi ke MutasiStok, hanya berupa kolom referensi tanpa foreign key.

Satu relasi pada diagram digambar putus-putus. MutasiStok hanya punya FK ke BatchProduk dan menyimpan nomor dokumen transaksi di kolom referensi selebar 40 karakter, bukan foreign key backend/stok/models.py:67-79. Karena itu integritas antara mutasi_stok dan transaksi dijaga di lapisan aplikasi, bukan oleh database. BatchProduk.barang_masuk sebaliknya memakai OneToOneField, sehingga satu BarangMasuk hanya melahirkan satu batch backend/stok/models.py:34-51.

Constraint CHECK dan unique

Semua constraint dideklarasikan di Meta.constraints dengan UniqueConstraint atau CheckConstraint backend/produk/models.py:56-70.

ConstraintTabelAturanSumber
satuan_unik_per_produksatuan_produkunique (produk, nama)backend/produk/models.py:68
isi_botol_positifsatuan_produkisi_botol >= 1backend/produk/models.py:69
harga_unik_per_tanggalharga_produkunique (satuan, berlaku_mulai)backend/produk/models.py:87-89
harga_tidak_negatifharga_produkharga >= 0backend/produk/models.py:90
sisa_tidak_minusbatch_produksisa >= 0backend/stok/models.py:48
batch_unik_per_produkbatch_produkunique (produk, nomor_batch)backend/stok/models.py:49
batch_unik_per_opnamestok_opname_itemunique (opname, batch)backend/stok/models.py:125
tagihan_tidak_minustransaksitotal >= total_retur + terbayarbackend/pembelian/models.py:84-87
terbayar_tidak_minustransaksiterbayar >= 0backend/pembelian/models.py:88
alokasi_unikitem_transaksi_batchunique (item, batch)backend/pembelian/models.py:130
retur_tidak_melebihi_alokasiitem_transaksi_batchjumlah_diretur <= jumlah_botolbackend/pembelian/models.py:131-134
pembayaran_positifpembayaranjumlah > 0backend/pembelian/models.py:158

Constraint ini adalah gerbang terakhir sebelum satu commit diterima. Diagram berikut menunjukkan empat CHECK yang harus dilalui bersama oleh satu transaksi penjualan beserta alokasi batch dan pembayarannya.

graph TD
  S["Simpan transaksi + alokasi batch + pembayaran"] --> C1{"terbayar_tidak_minus"}
  C1 -->|lolos| C2{"tagihan_tidak_minus"}
  C1 -->|gagal| X["commit ditolak"]
  C2 -->|lolos| C3{"retur_tidak_melebihi_alokasi"}
  C2 -->|gagal| X
  C3 -->|lolos| C4{"pembayaran_positif"}
  C3 -->|gagal| X
  C4 -->|lolos| OK["baris tersimpan"]
  C4 -->|gagal| X
  style S fill:#f59e0b,color:#000
  style C1 fill:#336791,color:#fff
  style C2 fill:#336791,color:#fff
  style C3 fill:#336791,color:#fff
  style C4 fill:#336791,color:#fff
  style OK fill:#2d7d46,color:#fff
  style X fill:#dc2626,color:#fff
%% fig: Gerbang constraint pada satu commit transaksi
%% desc: Setiap CHECK pada transaksi, alokasi batch, dan pembayaran harus lolos; satu kegagalan menolak seluruh commit.
Fig. 2: Gerbang constraint pada satu commit transaksiSetiap CHECK pada transaksi, alokasi batch, dan pembayaran harus lolos; satu kegagalan menolak seluruh commit.

⚠️ StokOpnameItem.selisih bertipe IntegerField polos. Satu-satunya constraint pada tabel itu adalah batch_unik_per_opname, jadi tidak ada CHECK yang memaksa selisih sama dengan stok_fisik - stok_tercatat; koreksinya harus dijaga lapisan aplikasi backend/stok/models.py:115-126.

⚠️ Di antara model yang dirujuk halaman ini, hanya StokOpnameItem.opname yang memakai on_delete=models.CASCADE. Relasi lain memakai PROTECT, sehingga menghapus satu baris StokOpname ikut menghapus itemnya, sedangkan menghapus batch atau produk justru ditolak backend/stok/models.py:115-126.

Kolom turunan, snapshot, dan kunci idempoten

sisa_tagihan bukan kolom biasa: nilainya dihitung database dari total - total_retur - terbayar dan disimpan permanen karena db_persist=True backend/pembelian/models.py:53-57. Untuk pemakaian di memori setelah objek diubah, tersedia property sisa yang menghitung ulang dari tiga atribut tersebut backend/pembelian/models.py:94-97.

Transaksi membawa tiga kolom pengaman tambahan: nomor unique, kunci_idempoten bertipe UUIDField dengan default=uuid.uuid4 dan unique=True, serta tanggal ber-db_index=True backend/pembelian/models.py:40-89. Retur juga memberi db_index=True pada tanggal dan nomor unique backend/pembelian/models.py:164-176.

ItemTransaksi menyimpan salinan nama_produk, nama_satuan, dan isi_botol di samping FK produk dan satuan, sehingga riwayat nota tetap terbaca setelah master produk berubah backend/pembelian/models.py:100-117. ReturItem mengikat pengembalian ke alokasi batch tertentu lewat FK alokasi, bukan langsung ke item transaksi, agar jumlah yang diretur dapat dihitung per batch backend/pembelian/models.py:187-196.

QuerySet khusus

Dua model memasang QuerySet sendiri. SatuanProduk memakai SatuanQuerySet.dengan_harga, yang mengambil HargaProduk terbaru dengan berlaku_mulai <= tanggal memakai Subquery dan OuterRef backend/produk/models.py:46-53. BatchProduk memakai BatchQuerySet.layak_jual, yang menyaring sisa > 0 dan kedaluwarsa >= timezone.localdate() backend/stok/models.py:29-31. Logika pemakaian FEFO ada di Alur Stok, Batch FEFO, dan Opname.

Nilai terenumerasi

JenisMutasi memuat tujuh nilai: masuk, penjualan, retur, rusak, kedaluwarsa, opname, dan batal backend/stok/models.py:57-64. StatusOpname memuat menunggu, disetujui, dan ditolak, dengan default menunggu pada kolom status StokOpname backend/stok/models.py:82-92. Tidak ada CHECK yang membatasi status di level database; batasannya hanya choices Django.

Penomoran dokumen berurutan

NomorUrut hanya punya dua kolom: kunci sebagai primary key dan terakhir yang bertipe PositiveIntegerField dengan default 0 backend/inti/models.py:4-18. Tabelnya bernama nomor_urut backend/inti/migrations/0001_initial.py:14-23. Fungsi nomor_berikutnya menaikkan terakhir lalu mengembalikan f"{awalan}-{baris.terakhir:0{lebar}d}" dengan lebar default 4 backend/inti/models.py:4-18.

Pemakaian nyata terlihat pada awalan per jenis dokumen: TRX-<tanggal> untuk transaksi backend/pembelian/layanan.py:120, BM-<tanggal> untuk barang masuk backend/stok/layanan.py:66, dan OP-<tanggal> untuk stok opname backend/stok/layanan.py:121. Awalan retur memakai lebar 3 digit backend/pembelian/layanan.py:120.

⚠️ Docstring nomor_berikutnya menyatakan fungsi ini harus dipanggil di dalam transaksi database backend/inti/models.py:4-18. Baris NomorUrut dikunci dengan select_for_update(); memanggilnya di luar transaksi membuat lock dilepas segera dan dua request bisa memperoleh nomor yang sama.

Migrasi dan index

Constraint tidak hanya hidup di model. Setiap migrasi awal mengulanginya sebagai operasi AddConstraint dan AddIndex: empat constraint produk di backend/produk/migrations/0001_initial.py:80-95, index serta constraint stok di backend/stok/migrations/0001_initial.py:104-119, dan lima constraint pembelian di backend/pembelian/migrations/0001_initial.py:158-177. Perubahan aturan harus ditulis di kedua tempat agar makemigrations tidak menghasilkan migrasi liar.

Index batch_produk_exp_idx pada pasangan kolom (produk, kedaluwarsa) dideklarasikan di Meta.indexes dan direalisasikan di migrasi backend/stok/migrations/0001_initial.py:104-119. Pasangan itu memang urutan yang dipakai BatchQuerySet.layak_jual dan Meta.ordering = ["kedaluwarsa", "id"] backend/stok/models.py:34-51.