Lompat ke konten
dokumentasi.id
Commit 32b2830
v2
Star0

Kamu membaca versi lama. Halaman ini hanya bisa dibaca.

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-45). 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:89), 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:5), lalu pembelian mengimpor model produk dan stok (backend/pembelian/models.py:8-9). 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-14).

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-126). 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-57).

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.
Fig. 1: Relasi tabel domain HydroSatu 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:

  • Produk menyimpan sku unik dan stok_minimum yang dihitung dalam botol (backend/produk/models.py:26-43). Satu produk memiliki banyak SatuanProduk dengan related_name="satuan", dan satu satuan memiliki banyak baris HargaProduk dengan related_name="riwayat_harga".
  • BatchProduk.barang_masuk adalah OneToOneField, jadi satu BarangMasuk menghasilkan tepat satu batch. BatchProduk.produk tetap disimpan terpisah agar batch bisa dibaca per produk tanpa menelusuri BarangMasuk (backend/stok/models.py:34-51).
  • MutasiStok hanya menunjuk batch dan menyimpan jumlah bertanda: positif untuk barang masuk, negatif untuk barang keluar, dalam botol (backend/stok/models.py:67-79). Tujuh nilai JenisMutasi yang sah adalah masuk, penjualan, retur, rusak, kedaluwarsa, opname, dan batal (backend/stok/models.py:57-64).
  • ItemTransaksiBatch berdiri di antara item dan batch. Pasangan (item, batch) unik, dan jumlah_diretur tidak boleh melebihi jumlah_botol (backend/pembelian/models.py:120-135). Mekanisme yang memilih batch dan mengisi tabel ini dibahas di Stok per Batch dan Alokasi FEFO.
  • Peran memakai kode sebagai primary key, dan Pengguna merujuknya lewat foreign key peran dengan PROTECT; properti izin membaca peta IZIN_PERAN berdasarkan peran_id (backend/akun/models.py:8-30). 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.

TabelKunci dan constraintRujukan
kategorinama unik, maksimum 80 karakterbackend/produk/models.py:9-18
produksku unik (maksimum 40), kategori PROTECT, stok_minimum default 0backend/produk/models.py:26-43
satuan_produksatuan_unik_per_produk pada (produk, nama), isi_botol_positifbackend/produk/models.py:56-70
harga_produkharga_unik_per_tanggal pada (satuan, berlaku_mulai), harga_tidak_negatifbackend/produk/models.py:76-91
batch_produksisa_tidak_minus, batch_unik_per_produk, index batch_produk_exp_idx pada (produk, kedaluwarsa)backend/stok/models.py:34-51
mutasi_stoktanpa constraint; jumlah bertanda dan sisa_sesudah hanya catatanbackend/stok/models.py:67-79
stok_opname_itembatch_unik_per_opname pada (opname, batch)backend/stok/models.py:115-126
pelanggantanpa constraint khusus; nama maksimum 160 karakterbackend/pembelian/models.py:12-25
transaksitagihan_tidak_minus dan terbayar_tidak_minusbackend/pembelian/models.py:83-89
item_transaksi_batchalokasi_unik pada (item, batch), retur_tidak_melebihi_alokasibackend/pembelian/models.py:120-135
perankode sebagai primary key, maksimum 30 karakterbackend/akun/models.py:8-30
penggunaperan PROTECT, kolom lain diwarisi AbstractUserbackend/akun/models.py:8-30
nomor_urutkunci sebagai primary key, terakhir default 0backend/inti/migrations/0001_initial.py:14-23

Tiga aturan angka yang paling sering menahan kesalahan:

  • Nilai isi_botol dijaga dua kali: MinValueValidator(1) di level field dan CheckConstraint bernama isi_botol_positif di level tabel (backend/produk/models.py:56-70). Harga produk disimpan sebagai DecimalField(max_digits=18, decimal_places=2) dan dijaga harga_tidak_negatif (backend/produk/models.py:76-91).
  • BatchProduk.sisa bertipe IntegerField karena harus bisa berkurang, bukan PositiveIntegerField. Batas bawahnya dipasang sebagai constraint sisa_tidak_minus, dan BatchQuerySet.layak_jual menyaring batch dengan sisa__gt=0 serta kedaluwarsa__gte=timezone.localdate() (backend/stok/models.py:29-31).
  • Transaksi menyimpan total, total_retur, dan terbayar, lalu sisa_tagihan dihitung database sebagai GeneratedField dengan db_persist=True dari total - total_retur - terbayar; properti sisa menghitung ulang nilai yang sama di memori agar aman dipakai setelah objek diubah (backend/pembelian/models.py:40-57). Constraint tagihan_tidak_minus menolak baris dengan total < total_retur + terbayar (backend/pembelian/models.py:83-89).

Properti sisa itu dipakai untuk piutang dan retur; alur lengkapnya ada di Transaksi Penjualan, Retur, dan Piutang.

⚠️ MutasiStok.sisa_sesudah hanya menyimpan hasil perhitungan saat mutasi dicatat; ia tidak dijaga constraint apa pun. Sumber kebenaran sisa stok tetap BatchProduk.sisa yang dijaga sisa_tidak_minus (backend/stok/models.py:34-51). Jangan memakai sisa_sesudah sebagai dasar keputusan stok. Urutan penulisan mutasi dan penguncian batch dibahas di Mutasi Stok dan Stok Opname.

⚠️ sisa_tagihan adalah GeneratedField yang dihitung dan disimpan database. Menulis nilai ke kolom itu langsung tidak mengubah apa pun; ubah total, total_retur, atau terbayar (backend/pembelian/models.py:40-57).

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-117). 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-53). Daftar produk memakainya di dalam Prefetch("satuan", SatuanProduk.objects.dengan_harga(hari_ini)) (backend/produk/views.py:67-73). 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-18). Tabel nomor_urut dibuat pada migration pertama app inti (backend/inti/migrations/0001_initial.py:14-23).

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.
Fig. 2: Nomor dokumen berurutan per awalanBarang 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:66). 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-120).

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-57). 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