Lompat ke konten
dokumentasi.id
Commit 32b2830
v2
Star0

3.1

Mutasi Stok dan Stok Opname

Stok per batch hanya berubah lewat satu fungsi: ubah_stok di backend/stok/layanan.py:21-44. 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-75).

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

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.
Fig. 1: Semua jenis mutasi menuju satu penulis sisaTujuh 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-44).

Jenis mutasi dan pemanggilnya

JenisMutasi mendefinisikan tujuh nilai, dari masuk sampai batal (backend/stok/models.py:57-64). Kolom jumlah bertanda: positif masuk, negatif keluar, dalam botol (backend/stok/models.py:67-75).

jenisArah jumlahPemanggil
masukpositif, jumlah_botolcatat_barang_masuk (backend/stok/layanan.py:53-88)
penjualannegatifalokasi FEFO di _buat_transaksi (backend/pembelian/layanan.py:147-156)
returpositifbuat_retur (backend/pembelian/layanan.py:259-261)
rusaknegatifkurangi_stok dan retur rusak (backend/pembelian/layanan.py:259-261)
kedaluwarsanegatifkurangi_stok (backend/stok/layanan.py:91-102)
opnameitem.selisihsetujui_opname (backend/stok/layanan.py:145-160)
batalpositif, botol yang belum diretursetujui_pembatalan (backend/pembelian/layanan.py:326-329)

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

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

Pembatalan transaksi hanya mengembalikan botol yang belum diretur, yaitu a.jumlah_botol - a.jumlah_diretur (backend/pembelian/layanan.py:326-329).

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

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-88, backend/inti/models.py:12-18).

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

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

Pengurangan barang rusak dan kedaluwarsa

Endpoint kurangi pada BatchViewSet memakai izin KELOLA_STOK dan mengembalikan BatchSerializer setelah layanan selesai (backend/stok/views.py:47-54). KurangiStokSerializer membatasi jenis hanya ke rusak dan kedaluwarsa, jumlah antara 1 dan 10.000.000, serta catatan wajib diisi (backend/stok/serializers.py:47-50).

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

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

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

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

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.
Fig. 2: Siklus status StokOpnameOpname 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-92).

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-160). 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-170).

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

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

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

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

Uji test_opname_ditolak_tidak_mengubah_stok memastikan penolakan atas fisik 1 dari 10 membiarkan sisa tetap 10 (backend/tests/test_stok.py:130-137).

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

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

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-99). Di IZIN_PERAN, hanya pemilik yang memegang SETUJUI_OPNAME; staf_gudang hanya punya LIHAT_PRODUK dan KELOLA_STOK (backend/inti/izin.py:31-35).

MethodPathIzinSumber
GET/api/batch/LIHAT_PRODUKBatchViewSet (backend/stok/views.py:24-45)
POST/api/batch/{id}/kurangi/KELOLA_STOKBatchViewSet.kurangi (backend/stok/views.py:47-54)
GET/api/mutasi/LIHAT_PRODUKMutasiViewSet (backend/stok/views.py:57-70)
GET, POST/api/barang-masuk/KELOLA_STOKBarangMasukViewSet (backend/stok/views.py:73-88)
GET, POST/api/opname/KELOLA_STOKOpnameViewSet (backend/stok/views.py:109-127)
POST/api/opname/{id}/setujui/SETUJUI_OPNAMEOpnameViewSet.setujui (backend/stok/views.py:109-127)
POST/api/opname/{id}/tolak/SETUJUI_OPNAMEOpnameViewSet.tolak (backend/stok/views.py:109-127)

Keempat rute didaftarkan router dengan basename batch, mutasi, barang-masuk, dan opname (backend/config/urls.py:27-30).

MutasiViewSet hanya menyediakan list, dengan filter produk, batch, dan jenis dari query parameter (backend/stok/views.py:57-70).

Penolakan aturan bisnis dipetakan ke HTTP 409 dengan badan {"kode", "pesan", "rincian"} lewat AturanBisnisError (backend/inti/galat.py:8-18).

⚠️ Jangan menulis batch.sisa secara langsung. Selain melewati ubah_stok, database menolak nilai negatif lewat check constraint sisa_tidak_minus (backend/stok/models.py:47-51).

⚠️ buat_opname dan setujui_opname memakai kunci_batch serta nomor_berikutnya, yang keduanya memerlukan transaksi database aktif. Memanggilnya di luar transaction.atomic menghilangkan jaminan urutan kunci (backend/stok/layanan.py:47-50).