3.1
Mutasi Stok dan Stok Opname
Stok per batch hanya berubah lewat satu fungsi: ubah_stok di backend/stok/layanan.py:21-441. Fungsi itu menambah atau mengurangi batch.sisa, menolak hasil negatif, lalu menulis satu baris MutasiStok yang menyimpan jumlah, sisa_sesudah, referensi, dan catatan yang dipotong ke 255 karakter (backend/stok/models.py:67-752).
Satu pintu perubahan sisa
Docstring ubah_stok menyebut syaratnya: baris batch harus sudah dikunci pemanggil. Karena itu pemanggil lebih dulu memanggil kunci_batch, yang menjalankan select_for_update() dengan order_by("id") supaya dua transaksi tidak saling menunggu (backend/stok/layanan.py:47-503).
graph LR
MASUK["masuk"] --> US["ubah_stok"]
PENJUALAN["penjualan"] --> US
RETUR["retur"] --> US
RUSAK["rusak"] --> US
KEDALUWARSA["kedaluwarsa"] --> US
OPNAME["opname"] --> US
BATAL["batal"] --> US
US --> SISA["batch.sisa"]
US --> MS["MutasiStok"]
style SISA fill:#336791,color:#fff
style MS fill:#336791,color:#fff
style US fill:#f59e0b,color:#000
%% fig: Semua jenis mutasi menuju satu penulis sisa
%% desc: Tujuh nilai JenisMutasi memanggil ubah_stok, yang memperbarui batch.sisa dan menulis jejak MutasiStok.Node ubah_stok adalah satu-satunya penulis batch.sisa; node MutasiStok adalah jejak auditnya. Pemeriksaan batch.sisa < 0 dilakukan sebelum save, jadi stok tidak pernah tersimpan minus (backend/stok/layanan.py:21-441).
Jenis mutasi dan pemanggilnya
JenisMutasi mendefinisikan tujuh nilai, dari masuk sampai batal (backend/stok/models.py:57-644). Kolom jumlah bertanda: positif masuk, negatif keluar, dalam botol (backend/stok/models.py:67-752).
jenis | Arah jumlah | Pemanggil |
|---|---|---|
masuk | positif, jumlah_botol | catat_barang_masuk (backend/stok/layanan.py:53-885) |
penjualan | negatif | alokasi FEFO di _buat_transaksi (backend/pembelian/layanan.py:147-1566) |
retur | positif | buat_retur (backend/pembelian/layanan.py:259-2617) |
rusak | negatif | kurangi_stok dan retur rusak (backend/pembelian/layanan.py:259-2617) |
kedaluwarsa | negatif | kurangi_stok (backend/stok/layanan.py:91-1028) |
opname | item.selisih | setujui_opname (backend/stok/layanan.py:145-1609) |
batal | positif, botol yang belum diretur | setujui_pembatalan (backend/pembelian/layanan.py:326-32910) |
Penjualan memotong batch satu per satu sampai perlu habis, dan setiap potongan menulis mutasi penjualan dengan nomor transaksi sebagai referensi (backend/pembelian/layanan.py:147-1566).
Retur memakai dua mutasi berurutan: barang layak menambah retur, lalu barang rusak mengurangi lagi dengan jenis rusak dan catatan Retur rusak (backend/pembelian/layanan.py:259-2617).
Pembatalan transaksi hanya mengembalikan botol yang belum diretur, yaitu a.jumlah_botol - a.jumlah_diretur (backend/pembelian/layanan.py:326-32910).
Barang masuk membentuk batch
catat_barang_masuk membuat BarangMasuk, lalu membuat BatchProduk baru dengan sisa=0, lalu memanggil ubah_stok dengan masuk.jumlah_botol berjenis masuk. jumlah_botol sendiri dihitung sebagai jumlah * satuan.isi_botol, dan jumlah_awal batch menyimpan nilai yang sama (backend/stok/layanan.py:53-885).
Nomor dokumen dibuat dengan nomor_berikutnya(f"BM-{tanggal:%Y%m%d}", 3), yang menghasilkan pola BM-YYYYMMDD-001 dari baris NomorUrut yang dikunci (backend/stok/layanan.py:53-885, backend/inti/models.py:12-1811).
Validasi masuk ada di BarangMasukBuatSerializer: satuan harus milik produk, kedaluwarsa harus setelah tanggal, dan nomor_batch tidak boleh sudah dipakai untuk produk yang sama (backend/stok/serializers.py:104-12612).
Uji test_barang_masuk_dalam_dus_disimpan_sebagai_botol mengirim 10 dus dan memeriksa jumlah_botol == 240, batch.sisa == 240, serta adanya mutasi masuk sebesar 240 (backend/tests/test_stok.py:13-3013).
Pengurangan barang rusak dan kedaluwarsa
Endpoint kurangi pada BatchViewSet memakai izin KELOLA_STOK dan mengembalikan BatchSerializer setelah layanan selesai (backend/stok/views.py:47-5414). KurangiStokSerializer membatasi jenis hanya ke rusak dan kedaluwarsa, jumlah antara 1 dan 10.000.000, serta catatan wajib diisi (backend/stok/serializers.py:47-5015).
kurangi_stok mengunci batch lebih dulu, menolak jumlah > batch.sisa dengan pesan Sisa batch {nomor_batch} hanya {sisa} botol. dan kode stok_minus, lalu memanggil ubah_stok dengan -jumlah (backend/stok/layanan.py:91-1028).
Uji test_kurangi_rusak_tercatat_tersendiri memotong 2 botol dari 5 sehingga sisa == 3 dan mutasi rusak bernilai -2; permintaan 9 botol berikutnya ditolak dengan status 409 (backend/tests/test_stok.py:72-8816).
Stok opname: catat selisih, putuskan terpisah
buat_opname mengunci seluruh batch yang dikirim, lalu menolak request bila ada batch yang masih menunggu di opname lain. Pesannya Batch ini masih ada di opname yang menunggu persetujuan: ... dengan kode opname_bentrok (backend/stok/layanan.py:105-13317).
Nomor opname memakai awalan OP-YYYYMMDD. Setiap StokOpnameItem menyimpan stok_tercatat (sisa saat hitung), stok_fisik, dan selisih = stok_fisik - stok_tercatat, dengan kombinasi opname dan batch yang unik (backend/stok/models.py:115-12518).
stateDiagram-v2
[*] --> menunggu : buat_opname
menunggu --> disetujui : setujui_opname dengan SETUJUI_OPNAME
menunggu --> ditolak : tolak_opname dengan SETUJUI_OPNAME
disetujui --> [*]
ditolak --> [*]
style menunggu fill:#f59e0b,color:#000
style disetujui fill:#2d7d46,color:#fff
style ditolak fill:#dc2626,color:#fff
%% fig: Siklus status StokOpname
%% desc: Opname lahir berstatus menunggu, lalu diputuskan menjadi disetujui atau ditolak oleh pemegang setujui_opname.Status awal selalu menunggu karena StokOpname.status memakai default=StatusOpname.MENUNGGU (backend/stok/models.py:82-9219).
setujui_opname menerapkan item.selisih untuk setiap item yang selisihnya bukan nol, memakai jenis opname dan referensi berisi opname.nomor, lalu mengisi diputuskan_oleh dan diputuskan_pada (backend/stok/layanan.py:145-1609). Docstring-nya menegaskan bahwa selisih saat hitung yang diterapkan, sehingga penjualan setelah hitung tetap terbawa.
tolak_opname hanya mengubah status menjadi ditolak beserta kolom keputusan; stok tidak disentuh sama sekali (backend/stok/layanan.py:163-17020).
Kedua fungsi memakai _ambil_opname_menunggu, yang mengunci baris opname dan menolak opname yang sudah diputuskan dengan kode opname_selesai (backend/stok/layanan.py:136-14221).
Uji test_opname_menerapkan_selisih_saat_disetujui mencatat fisik 7 dari tercatat 10 sehingga selisih == -3, dan batch.sisa tetap 10 sebelum persetujuan (backend/tests/test_stok.py:91-11222).
Masih di uji yang sama, klien_gudang ditolak 403 saat memanggil setujui, klien_pemilik berhasil mengubah status menjadi disetujui dan sisa menjadi 7, lalu persetujuan kedua dijawab 409 (backend/tests/test_stok.py:91-11222).
Uji test_opname_memperhitungkan_penjualan_setelah_hitung menjual 4 botol setelah opname dicatat, dan setelah persetujuan sisa menjadi 5 (backend/tests/test_stok.py:115-12723).
Uji test_opname_ditolak_tidak_mengubah_stok memastikan penolakan atas fisik 1 dari 10 membiarkan sisa tetap 10 (backend/tests/test_stok.py:130-13724).
Uji test_opname_ganda_pada_batch_yang_sama_ditolak mengirim opname kedua untuk batch yang sama dan menerima 409 dengan kode == "opname_bentrok" (backend/tests/test_stok.py:140-14625).
OpnameBuatSerializer juga menolak batch ganda di dalam satu request dengan pesan Batch tidak boleh ganda. dan mewajibkan items tidak kosong (backend/stok/serializers.py:173-18626).
Izin dan endpoint
Izin opname dipisah: list, retrieve, dan create memakai KELOLA_STOK, sedangkan setujui dan tolak memakai SETUJUI_OPNAME (backend/stok/views.py:91-9927). Di IZIN_PERAN, hanya pemilik yang memegang SETUJUI_OPNAME; staf_gudang hanya punya LIHAT_PRODUK dan KELOLA_STOK (backend/inti/izin.py:31-3528).
| Method | Path | Izin | Sumber |
|---|---|---|---|
| GET | /api/batch/ | LIHAT_PRODUK | BatchViewSet (backend/stok/views.py:24-4529) |
| POST | /api/batch/{id}/kurangi/ | KELOLA_STOK | BatchViewSet.kurangi (backend/stok/views.py:47-5414) |
| GET | /api/mutasi/ | LIHAT_PRODUK | MutasiViewSet (backend/stok/views.py:57-7030) |
| GET, POST | /api/barang-masuk/ | KELOLA_STOK | BarangMasukViewSet (backend/stok/views.py:73-8831) |
| GET, POST | /api/opname/ | KELOLA_STOK | OpnameViewSet (backend/stok/views.py:109-12732) |
| POST | /api/opname/{id}/setujui/ | SETUJUI_OPNAME | OpnameViewSet.setujui (backend/stok/views.py:109-12732) |
| POST | /api/opname/{id}/tolak/ | SETUJUI_OPNAME | OpnameViewSet.tolak (backend/stok/views.py:109-12732) |
Keempat rute didaftarkan router dengan basename batch, mutasi, barang-masuk, dan opname (backend/config/urls.py:27-3033).
MutasiViewSet hanya menyediakan list, dengan filter produk, batch, dan jenis dari query parameter (backend/stok/views.py:57-7030).
Penolakan aturan bisnis dipetakan ke HTTP 409 dengan badan {"kode", "pesan", "rincian"} lewat AturanBisnisError (backend/inti/galat.py:8-1834).
⚠️ Jangan menulis
batch.sisasecara langsung. Selain melewatiubah_stok, database menolak nilai negatif lewat check constraintsisa_tidak_minus(backend/stok/models.py:47-5135).
⚠️
buat_opnamedansetujui_opnamememakaikunci_batchsertanomor_berikutnya, yang keduanya memerlukan transaksi database aktif. Memanggilnya di luartransaction.atomicmenghilangkan jaminan urutan kunci (backend/stok/layanan.py:47-503).