2.1
Modul Domain dan Model Data
hydro-app memisahkan domain menjadi enam app Django yang terdaftar di INSTALLED_APPS: inti, akun, produk, stok, pembelian, dan laporan (backend/config/settings.py:26-451). Setiap model menetapkan db_table sendiri di dalam Meta, sehingga nama tabel memakai bahasa Indonesia bertanda garis bawah, bukan pola bawaan Django app_model. Model pengguna kustom ditunjuk lewat AUTH_USER_MODEL = "akun.Pengguna" (backend/config/settings.py:892), dan kolom jejak pelaku seperti dibuat_oleh selalu memakai settings.AUTH_USER_MODEL, bukan User bawaan.
Arah ketergantungan model dijaga satu arah. App stok mengimpor Produk dan SatuanProduk milik produk (backend/stok/models.py:53), lalu pembelian mengimpor model produk dan stok (backend/pembelian/models.py:8-94). App laporan tidak punya models.py; ia hanya membaca ItemTransaksi, Retur, ReturItem, Transaksi, Produk, dan BatchProduk yang didefinisikan app lain (backend/laporan/data.py:11-145).
Relasi dan kardinalitas tabel
Diagram berikut memperlihatkan kunci, arah foreign key, dan kardinalitas antartabel domain. Semua foreign key memakai on_delete=models.PROTECT, kecuali StokOpnameItem.opname yang memakai CASCADE (backend/stok/models.py:115-1266). Konsekuensinya, produk, satuan, batch, dan pengguna tidak bisa dihapus selama masih dirujuk baris lain. Penerapan strategi penghapusan ini terlihat pada Transaksi.pelanggan yang menunjuk Pelanggan dengan PROTECT (backend/pembelian/models.py:40-577).
erDiagram
KATEGORI ||--o{ PRODUK : "kategori"
PRODUK ||--o{ SATUAN_PRODUK : "satuan"
SATUAN_PRODUK ||--o{ HARGA_PRODUK : "riwayat_harga"
PRODUK ||--o{ BATCH_PRODUK : "batch"
SATUAN_PRODUK ||--o{ BARANG_MASUK : "diterima"
BARANG_MASUK ||--|| BATCH_PRODUK : "barang_masuk"
BATCH_PRODUK ||--o{ MUTASI_STOK : "mutasi"
PELANGGAN ||--o{ TRANSAKSI : "transaksi"
TRANSAKSI ||--o{ ITEM_TRANSAKSI : "items"
ITEM_TRANSAKSI ||--o{ ITEM_TRANSAKSI_BATCH : "alokasi"
BATCH_PRODUK ||--o{ ITEM_TRANSAKSI_BATCH : "alokasi"
PERAN ||--o{ PENGGUNA : "peran"
PENGGUNA ||--o{ TRANSAKSI : "transaksi_dibuat"
PRODUK {
varchar40 sku UK
varchar160 nama
integer kategori_id FK
integer stok_minimum
}
SATUAN_PRODUK {
integer produk_id FK
varchar30 nama
integer isi_botol
}
HARGA_PRODUK {
integer satuan_id FK
decimal18_2 harga
date berlaku_mulai
}
BATCH_PRODUK {
integer produk_id FK
integer barang_masuk_id FK
date kedaluwarsa
integer jumlah_awal
integer sisa
}
MUTASI_STOK {
integer batch_id FK
varchar20 jenis
integer jumlah
integer sisa_sesudah
}
TRANSAKSI {
varchar30 nomor UK
uuid kunci_idempoten UK
varchar16 status
decimal18_2 total
decimal18_2 sisa_tagihan
}
ITEM_TRANSAKSI {
integer transaksi_id FK
integer satuan_id FK
integer jumlah
integer isi_botol
decimal18_2 subtotal
}
ITEM_TRANSAKSI_BATCH {
integer item_id FK
integer batch_id FK
integer jumlah_botol
integer jumlah_diretur
}
PENGGUNA {
varchar120 nama
varchar30 peran_id FK
}
%% fig: Relasi tabel domain Hydro
%% desc: Satu produk memuat banyak satuan dan batch, mutasi stok menempel ke batch, dan alokasi penjualan menghubungkan item transaksi ke batch yang dipakai.Beberapa kardinalitas perlu dibaca lebih teliti:
Produkmenyimpanskuunik danstok_minimumyang dihitung dalam botol (backend/produk/models.py:26-438). Satu produk memiliki banyakSatuanProdukdenganrelated_name="satuan", dan satu satuan memiliki banyak barisHargaProdukdenganrelated_name="riwayat_harga".BatchProduk.barang_masukadalahOneToOneField, jadi satuBarangMasukmenghasilkan tepat satu batch.BatchProduk.produktetap disimpan terpisah agar batch bisa dibaca per produk tanpa menelusuriBarangMasuk(backend/stok/models.py:34-519).MutasiStokhanya menunjuk batch dan menyimpanjumlahbertanda: positif untuk barang masuk, negatif untuk barang keluar, dalam botol (backend/stok/models.py:67-7910). Tujuh nilaiJenisMutasiyang sah adalahmasuk,penjualan,retur,rusak,kedaluwarsa,opname, danbatal(backend/stok/models.py:57-6411).ItemTransaksiBatchberdiri di antara item dan batch. Pasangan (item,batch) unik, danjumlah_direturtidak boleh melebihijumlah_botol(backend/pembelian/models.py:120-13512). Mekanisme yang memilih batch dan mengisi tabel ini dibahas di Stok per Batch dan Alokasi FEFO.Peranmemakaikodesebagai primary key, danPenggunamerujuknya lewat foreign keyperandenganPROTECT; propertiizinmembaca petaIZIN_PERANberdasarkanperan_id(backend/akun/models.py:8-3013). Pemberian izin dan 2FA dijelaskan di Autentikasi, 2FA, dan Izin.
Constraint yang menjaga angka tetap sah
Tabel berikut memuat nama constraint yang benar-benar tertulis di kode. Hampir semuanya dipasang di Meta.constraints, jadi database ikut menolak baris yang melanggar, bukan hanya serializer.
| Tabel | Kunci dan constraint | Rujukan |
|---|---|---|
kategori | nama unik, maksimum 80 karakter | backend/produk/models.py:9-1814 |
produk | sku unik (maksimum 40), kategori PROTECT, stok_minimum default 0 | backend/produk/models.py:26-438 |
satuan_produk | satuan_unik_per_produk pada (produk, nama), isi_botol_positif | backend/produk/models.py:56-7015 |
harga_produk | harga_unik_per_tanggal pada (satuan, berlaku_mulai), harga_tidak_negatif | backend/produk/models.py:76-9116 |
batch_produk | sisa_tidak_minus, batch_unik_per_produk, index batch_produk_exp_idx pada (produk, kedaluwarsa) | backend/stok/models.py:34-519 |
mutasi_stok | tanpa constraint; jumlah bertanda dan sisa_sesudah hanya catatan | backend/stok/models.py:67-7910 |
stok_opname_item | batch_unik_per_opname pada (opname, batch) | backend/stok/models.py:115-1266 |
pelanggan | tanpa constraint khusus; nama maksimum 160 karakter | backend/pembelian/models.py:12-2517 |
transaksi | tagihan_tidak_minus dan terbayar_tidak_minus | backend/pembelian/models.py:83-8918 |
item_transaksi_batch | alokasi_unik pada (item, batch), retur_tidak_melebihi_alokasi | backend/pembelian/models.py:120-13512 |
peran | kode sebagai primary key, maksimum 30 karakter | backend/akun/models.py:8-3013 |
pengguna | peran PROTECT, kolom lain diwarisi AbstractUser | backend/akun/models.py:8-3013 |
nomor_urut | kunci sebagai primary key, terakhir default 0 | backend/inti/migrations/0001_initial.py:14-2319 |
Tiga aturan angka yang paling sering menahan kesalahan:
- Nilai
isi_botoldijaga dua kali:MinValueValidator(1)di level field danCheckConstraintbernamaisi_botol_positifdi level tabel (backend/produk/models.py:56-7015). Harga produk disimpan sebagaiDecimalField(max_digits=18, decimal_places=2)dan dijagaharga_tidak_negatif(backend/produk/models.py:76-9116). BatchProduk.sisabertipeIntegerFieldkarena harus bisa berkurang, bukanPositiveIntegerField. Batas bawahnya dipasang sebagai constraintsisa_tidak_minus, danBatchQuerySet.layak_jualmenyaring batch dengansisa__gt=0sertakedaluwarsa__gte=timezone.localdate()(backend/stok/models.py:29-3120).Transaksimenyimpantotal,total_retur, danterbayar, lalusisa_tagihandihitung database sebagaiGeneratedFielddengandb_persist=Truedaritotal - total_retur - terbayar; propertisisamenghitung ulang nilai yang sama di memori agar aman dipakai setelah objek diubah (backend/pembelian/models.py:40-577). Constrainttagihan_tidak_minusmenolak baris dengantotal < total_retur + terbayar(backend/pembelian/models.py:83-8918).
Properti sisa itu dipakai untuk piutang dan retur; alur lengkapnya ada di Transaksi Penjualan, Retur, dan Piutang.
⚠️
MutasiStok.sisa_sesudahhanya menyimpan hasil perhitungan saat mutasi dicatat; ia tidak dijaga constraint apa pun. Sumber kebenaran sisa stok tetapBatchProduk.sisayang dijagasisa_tidak_minus(backend/stok/models.py:34-519). Jangan memakaisisa_sesudahsebagai dasar keputusan stok. Urutan penulisan mutasi dan penguncian batch dibahas di Mutasi Stok dan Stok Opname.
⚠️
sisa_tagihanadalahGeneratedFieldyang dihitung dan disimpan database. Menulis nilai ke kolom itu langsung tidak mengubah apa pun; ubahtotal,total_retur, atauterbayar(backend/pembelian/models.py:40-577).
ItemTransaksi sengaja menyimpan salinan nama_produk, nama_satuan, isi_botol, dan harga_satuan saat transaksi dibuat, di samping foreign key ke Produk dan SatuanProduk (backend/pembelian/models.py:100-11721). Karena itu, mengubah nama produk atau menambah harga baru tidak menulis ulang nilai transaksi lama. Aturan riwayat harga dan satuannya dijelaskan di Produk, Satuan, dan Riwayat Harga.
Pembacaan turunan harga per tanggal
SatuanQuerySet.dengan_harga(tanggal) mengannotate setiap satuan dengan harga terbaru yang berlaku_mulai__lte tanggal tersebut, memakai Subquery berurut -berlaku_mulai dan diambil satu baris (backend/produk/models.py:46-5322). Daftar produk memakainya di dalam Prefetch("satuan", SatuanProduk.objects.dengan_harga(hari_ini)) (backend/produk/views.py:67-7323). Nilai harga hasil annotate ini data turunan, bukan kolom tabel, jadi tidak pernah masuk citations constraint harga_produk.
Nomor dokumen dan kunci idempoten
Nomor dokumen tidak memakai sequence per tabel, melainkan satu baris NomorUrut per awalan. Fungsi nomor_berikutnya melakukan get_or_create pada kunci, mengambil ulang baris dengan select_for_update(), menaikkan terakhir, lalu mengembalikan f"{awalan}-{baris.terakhir:0{lebar}d}" dengan lebar bawaan 4. Docstring-nya menegaskan fungsi ini harus dipanggil di dalam transaksi database (backend/inti/models.py:12-1824). Tabel nomor_urut dibuat pada migration pertama app inti (backend/inti/migrations/0001_initial.py:14-2319).
graph LR
BM["stok.layanan.catat_barang_masuk"] --> N["nomor_berikutnya(awalan, lebar)"]
TRX["pembelian.layanan.buat_transaksi"] --> N
N --> L["NomorUrut.select_for_update()"]
L --> DB[("nomor_urut")]
style BM fill:#2d7d46,color:#fff
style TRX fill:#2d7d46,color:#fff
style N fill:#f59e0b,color:#000
style L fill:#f59e0b,color:#000
style DB fill:#336791,color:#fff
%% fig: Nomor dokumen berurutan per awalan
%% desc: Barang masuk dan transaksi meminta nomor ke abstraksi NomorUrut, yang mengunci satu baris nomor_urut sebelum menaikkan nilai terakhir.Pemanggil memilih awalan dan lebar sendiri. stok.layanan.catat_barang_masuk memakai nomor_berikutnya(f"BM-{tanggal:%Y%m%d}", 3) sehingga nomor barang masuk berbentuk BM-20260101-001 (backend/stok/layanan.py:6625). pembelian.layanan.buat_transaksi memakai nomor_berikutnya(f"TRX-{tanggal:%Y%m%d}") tanpa argumen lebar, sehingga lebar yang dipakai adalah 4 (backend/pembelian/layanan.py:119-12026).
Di sisi transaksi penjualan, pengulangan request ditahan oleh kunci_idempoten, sebuah UUIDField unik dengan default=uuid.uuid4 dan editable=False (backend/pembelian/models.py:40-577). Kolom ini yang membuat satu penekanan tombol dua kali tidak menjadi dua transaksi; penanganannya dijelaskan di Transaksi Penjualan, Retur, dan Piutang.
Kebiasaan yang konsisten di seluruh model
- Setiap
Metamenetapkandb_tableeksplisit, misalnyasatuan_produkdanitem_transaksi_batch(backend/produk/models.py:56-7015, backend/pembelian/models.py:120-13512). - Setiap tabel transaksi mengurutkan bawaan lewat
orderingmenurun pada waktu dibuat, misalnya["-dibuat_pada", "-id"]untukmutasi_stok(backend/stok/models.py:67-7910). dibuat_olehselalu non-null kecuali padaHargaProdukyang membolehkannull=True(backend/produk/models.py:76-9116), dan seluruh field itu memakaisettings.AUTH_USER_MODELyang berujung keakun.Pengguna(backend/config/settings.py:892).- Perubahan stok tidak disimpan sebagai kolom biasa di
Produk; sisa hidup diBatchProdukdan jejaknya diMutasiStok(backend/stok/models.py:34-519). Cara membacanya lewat laporan ada di Laporan dan Ekspor Berkas.