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-791. 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
Peran memakai kode sebagai primary key, bukan id otomatis backend/akun/models.py:8-1718. Tiga baris peran awal diisi lewat migrasi data RunPython backend/akun/migrations/0002_peran_awal.py:3-1321, sedangkan Pengguna memperluas AbstractUser dengan kolom nama dan FK peran ber-on_delete=PROTECT backend/akun/models.py:20-4219. 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.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-798. 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-517.
Constraint CHECK dan unique
Semua constraint dideklarasikan di Meta.constraints dengan UniqueConstraint atau CheckConstraint backend/produk/models.py:56-704.
| Constraint | Tabel | Aturan | Sumber |
|---|---|---|---|
satuan_unik_per_produk | satuan_produk | unique (produk, nama) | backend/produk/models.py:6822 |
isi_botol_positif | satuan_produk | isi_botol >= 1 | backend/produk/models.py:6923 |
harga_unik_per_tanggal | harga_produk | unique (satuan, berlaku_mulai) | backend/produk/models.py:87-8924 |
harga_tidak_negatif | harga_produk | harga >= 0 | backend/produk/models.py:9025 |
sisa_tidak_minus | batch_produk | sisa >= 0 | backend/stok/models.py:4826 |
batch_unik_per_produk | batch_produk | unique (produk, nomor_batch) | backend/stok/models.py:4927 |
batch_unik_per_opname | stok_opname_item | unique (opname, batch) | backend/stok/models.py:12528 |
tagihan_tidak_minus | transaksi | total >= total_retur + terbayar | backend/pembelian/models.py:84-8729 |
terbayar_tidak_minus | transaksi | terbayar >= 0 | backend/pembelian/models.py:8830 |
alokasi_unik | item_transaksi_batch | unique (item, batch) | backend/pembelian/models.py:13031 |
retur_tidak_melebihi_alokasi | item_transaksi_batch | jumlah_diretur <= jumlah_botol | backend/pembelian/models.py:131-13432 |
pembayaran_positif | pembayaran | jumlah > 0 | backend/pembelian/models.py:15833 |
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.⚠️
StokOpnameItem.selisihbertipeIntegerFieldpolos. Satu-satunya constraint pada tabel itu adalahbatch_unik_per_opname, jadi tidak ada CHECK yang memaksaselisihsama denganstok_fisik - stok_tercatat; koreksinya harus dijaga lapisan aplikasi backend/stok/models.py:115-12610.
⚠️ Di antara model yang dirujuk halaman ini, hanya
StokOpnameItem.opnameyang memakaion_delete=models.CASCADE. Relasi lain memakaiPROTECT, sehingga menghapus satu barisStokOpnameikut menghapus itemnya, sedangkan menghapus batch atau produk justru ditolak backend/stok/models.py:115-12610.
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-5734. Untuk pemakaian di memori setelah objek diubah, tersedia property sisa yang menghitung ulang dari tiga atribut tersebut backend/pembelian/models.py:94-9735.
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-8912. Retur juga memberi db_index=True pada tanggal dan nomor unique backend/pembelian/models.py:164-17616.
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-11713. 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-19617.
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-5336. BatchProduk memakai BatchQuerySet.layak_jual, yang menyaring sisa > 0 dan kedaluwarsa >= timezone.localdate() backend/stok/models.py:29-3137. 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-6438. StatusOpname memuat menunggu, disetujui, dan ditolak, dengan default menunggu pada kolom status StokOpname backend/stok/models.py:82-929. 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-1820. Tabelnya bernama nomor_urut backend/inti/migrations/0001_initial.py:14-2339. Fungsi nomor_berikutnya menaikkan terakhir lalu mengembalikan f"{awalan}-{baris.terakhir:0{lebar}d}" dengan lebar default 4 backend/inti/models.py:4-1820.
Pemakaian nyata terlihat pada awalan per jenis dokumen: TRX-<tanggal> untuk transaksi backend/pembelian/layanan.py:12040, BM-<tanggal> untuk barang masuk backend/stok/layanan.py:6641, dan OP-<tanggal> untuk stok opname backend/stok/layanan.py:12142. Awalan retur memakai lebar 3 digit backend/pembelian/layanan.py:12040.
⚠️ Docstring
nomor_berikutnyamenyatakan fungsi ini harus dipanggil di dalam transaksi database backend/inti/models.py:4-1820. BarisNomorUrutdikunci denganselect_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-9543, index serta constraint stok di backend/stok/migrations/0001_initial.py:104-11944, dan lima constraint pembelian di backend/pembelian/migrations/0001_initial.py:158-17745. 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-11944. Pasangan itu memang urutan yang dipakai BatchQuerySet.layak_jual dan Meta.ordering = ["kedaluwarsa", "id"] backend/stok/models.py:34-517.