Lompat ke konten
dokumentasi.id
Commit 32b2830
v2
Star0

Kamu membaca versi lama. Halaman ini hanya bisa dibaca.

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