Lompat ke konten
dokumentasi.id
Commit 32b2830
v2
Star0

6

Laporan dan Ekspor Berkas

Modul backend/laporan menyediakan empat laporan sekaligus: penjualan, produk terlaris, stok, dan piutang. Keempatnya tidak menulis ulang logika response, karena semuanya diturunkan dari satu kelas dasar LaporanView. Kelas dasar itu hanya menyimpan tiga hal: izin LIHAT_LAPORAN, atribut tampilan (judul, nama_file, kolom), dan satu method ambil_baris yang wajib diisi subkelas. Method get yang menentukan bentuk response ada di kelas dasar, sedangkan serializer_class baru dideklarasikan di masing-masing subkelas. backend/laporan/views.py:45-61

Satu get untuk tiga bentuk response

Method get memanggil ambil_baris lebih dulu, lalu membaca query param ekspor. Nilai xlsx diarahkan ke ekspor.ke_xlsx, nilai pdf ke ekspor.ke_pdf dengan tambahan parameter periode, dan nilai lain apa pun jatuh ke Response(self.serializer_class(baris, many=True).data). Artinya ekspor tidak divalidasi oleh view: nilai asing seperti ekspor=csv tidak menghasilkan error, melainkan response JSON biasa. backend/laporan/views.py:54-61

graph TD
    req["GET /api/laporan/..."] --> guard{"PunyaIzin: LIHAT_LAPORAN"}
    guard -->|"izin cocok"| get["LaporanView.get"]
    guard -->|"izin tidak ada"| tolak["permintaan ditolak"]
    get --> ambil["ambil_baris() subkelas"]
    ambil --> periode{"butuh periode?"}
    periode -->|"penjualan, terlaris"| pr["ambil_periode: dari, sampai"]
    periode -->|"stok, piutang"| kosong["periode = string kosong"]
    pr --> baris["list of dict dari data.py"]
    kosong --> baris
    baris --> pilih{"query param ekspor"}
    pilih -->|"kosong atau nilai lain"| json["Response serializer_class many"]
    pilih -->|"xlsx"| xlsx["ekspor.ke_xlsx"]
    pilih -->|"pdf"| pdf["ekspor.ke_pdf"]
    pr -.->|"dari lewat sampai"| err["ValidationError 400"]
    style req fill:#f59e0b,color:#000
    style guard fill:#f59e0b,color:#000
    style get fill:#2d7d46,color:#fff
    style tolak fill:#dc2626,color:#fff
    style ambil fill:#336791,color:#fff
    style periode fill:#f59e0b,color:#000
    style pr fill:#336791,color:#fff
    style kosong fill:#336791,color:#fff
    style baris fill:#336791,color:#fff
    style pilih fill:#f59e0b,color:#000
    style json fill:#2d7d46,color:#fff
    style xlsx fill:#8b5cf6,color:#fff
    style pdf fill:#8b5cf6,color:#fff
    style err fill:#dc2626,color:#fff
%% fig: Cabang response laporan
%% desc: Satu method get memilih antara response JSON, berkas xlsx, dan berkas PDF berdasarkan query param ekspor.
Fig. 1: Cabang response laporanSatu method get memilih antara response JSON, berkas xlsx, dan berkas PDF berdasarkan query param ekspor.

Keempat endpoint didaftarkan langsung di config/urls.py, bukan lewat router. backend/config/urls.py:43-46

PathViewjudulnama_fileParameter yang didokumentasikan
api/laporan/penjualan/LaporanPenjualanViewLaporan penjualanlaporan-penjualandari, sampai, per, ekspor
api/laporan/produk-terlaris/LaporanTerlarisViewProduk terlarisproduk-terlarisdari, sampai, ekspor
api/laporan/stok/LaporanStokViewLaporan stoklaporan-stokekspor
api/laporan/piutang/LaporanPiutangViewLaporan piutanglaporan-piutangekspor

Dekorator extend_schema di tiap subkelas menyusun daftar parameter dari dua konstanta bersama, PARAM_PERIODE dan PARAM_EKSPOR, lalu menambahkan per khusus laporan penjualan. PARAM_EKSPOR mendeklarasikan enum xlsx dan pdf. backend/laporan/views.py:29-33 backend/laporan/views.py:85-92

Parameter periode dan pengelompokan

ambil_periode membaca dari dan sampai lewat parse_date. Bila tidak diisi atau tidak terparse, dari menjadi hari pertama bulan berjalan (hari_ini.replace(day=1)) dan sampai menjadi hari ini. Urutan terbalik ditolak dengan ValidationError yang memakai kunci dari. backend/laporan/views.py:36-42

⚠️ Pesan error periode muncul sebagai {"dari": ["Tanggal awal setelah tanggal akhir."]} dengan status 400, dan test-nya menegaskan status itu untuk dari=2026-02-01 serta sampai=2026-01-01. Klien yang hanya mengirim sampai tanpa dari justru mendapat rentang dari awal bulan, bukan rentang dari awal data. backend/tests/test_laporan.py:53-55

ParameterDipakai diNilai yang diterimaPerilaku bawaan
daripenjualan, produk terlaristanggal hasil parse_datehari pertama bulan berjalan
sampaipenjualan, produk terlaristanggal hasil parse_datehari ini menurut timezone.localdate()
perpenjualanbulan mengaktifkan TruncMonth; nilai lain menjadi harihari
eksporkeempat laporanxlsx, pdfresponse JSON

per dibaca dengan perbandingan ketat: hanya string bulan yang mengubah pengelompokan, selebihnya hari. backend/laporan/views.py:80-83 Test test_laporan_per_bulan memastikan periode pada baris pertama sama dengan tanggal 1 bulan berjalan. backend/tests/test_laporan.py:46-50

Label periode pada PDF dibuat oleh teks_periode dengan bentuk dd/mm/yyyy s.d. dd/mm/yyyy. Laporan stok dan piutang mengembalikan string kosong sebagai periode, sehingga kepala PDF-nya hanya memuat nama perusahaan, garis pemisah, dan waktu cetak. backend/laporan/views.py:64-65 backend/laporan/views.py:134-135 backend/laporan/views.py:158-159

Sumber angka tiap laporan

Empat fungsi di data.py memikul seluruh query. Tidak ada view yang menulis ORM sendiri; laporan hanya merapikan hasil fungsi-fungsi ini. backend/laporan/views.py:45-61

graph TD
    transaksi["Transaksi tanpa DIBATALKAN"] --> penjualan["data.penjualan"]
    retur["Retur"] --> penjualan
    item["ItemTransaksi"] --> terlaris["data.produk_terlaris"]
    returitem["ReturItem"] --> terlaris
    batch["BatchProduk sisa lebih dari nol"] --> stok["data.stok"]
    produk["Produk aktif"] --> stok
    transaksi --> piutang["data.piutang"]
    penjualan --> vjual["LaporanPenjualanView"]
    terlaris --> vterlaris["LaporanTerlarisView"]
    stok --> vstok["LaporanStokView"]
    piutang --> vpiutang["LaporanPiutangView"]
    penjualan --> dasbor["DasborView"]
    terlaris --> dasbor
    style transaksi fill:#336791,color:#fff
    style retur fill:#336791,color:#fff
    style item fill:#336791,color:#fff
    style returitem fill:#336791,color:#fff
    style batch fill:#336791,color:#fff
    style produk fill:#336791,color:#fff
    style penjualan fill:#8b5cf6,color:#fff
    style terlaris fill:#8b5cf6,color:#fff
    style stok fill:#8b5cf6,color:#fff
    style piutang fill:#8b5cf6,color:#fff
    style vjual fill:#2d7d46,color:#fff
    style vterlaris fill:#2d7d46,color:#fff
    style vstok fill:#2d7d46,color:#fff
    style vpiutang fill:#2d7d46,color:#fff
    style dasbor fill:#2d7d46,color:#fff
%% fig: Model dan fungsi data yang memasok tiap laporan
%% desc: Setiap laporan membaca satu fungsi di data.py, dan fungsi itu hanya menyentuh model transaksi, retur, produk, serta batch.
Fig. 2: Model dan fungsi data yang memasok tiap laporanSetiap laporan membaca satu fungsi di data.py, dan fungsi itu hanya menyentuh model transaksi, retur, produk, serta batch.
  • data.penjualan memotong periode dengan TruncMonth atau TruncDay, menghitung jumlah dan bruto dari transaksi sah, lalu mengurangi retur pada tanggal returnya, bukan tanggal nota. Dokstring fungsi ini menyatakan aturan tersebut secara eksplisit. Transaksi berstatus DIBATALKAN disaring lewat helper _transaksi_sah. backend/laporan/data.py:19-20 backend/laporan/data.py:23-60
  • data.produk_terlaris menjumlahkan Sum(F("jumlah") * F("isi_botol")) sebagai botol dan Sum("subtotal") sebagai nilai, lalu menguranginya dengan ReturItem yang sepadan. Hasil diurutkan menurun berdasarkan botol_terjual; argumen batas memotong daftar, dan DasborView memakainya dengan batas=5. backend/laporan/data.py:63-90 backend/laporan/views.py:223
  • data.stok membangun baris dari dengan_stok, sehingga stok_botol hanya menghitung batch yang masih layak jual, sementara stok_kedaluwarsa menghitung sisa batch yang sudah lewat tanggal. Kolom status berasal dari status_stok yang mengembalikan habis, menipis, atau aman. backend/laporan/data.py:93-108 backend/produk/layanan.py:15-34 backend/produk/layanan.py:37-40
  • data.piutang mengambil transaksi sah dengan sisa_tagihan__gt=0, mengurutkan menurut jatuh_tempo lalu tanggal, dan menghitung terlambat_hari sebagai max((hari_ini - t.jatuh_tempo).days, 0) dengan nilai 0 bila jatuh_tempo kosong. backend/laporan/data.py:111-133

data.py juga menyediakan batch_hampir_kedaluwarsa dan tren_penjualan yang tidak dipakai endpoint laporan mana pun, melainkan oleh DasborView. tren_penjualan mengisi hari tanpa penjualan dengan NOL agar panjang deretnya selalu 14 hari. backend/laporan/data.py:136-153

Bentuk baris dan definisi kolom

Tipe Kolom adalah alias tuple[str, str, str] untuk tiga posisi tetap: kunci data, label tampilan, dan jenis (teks, angka, uang, tanggal). Jenis inilah satu-satunya penentu format sel di xlsx dan perataan angka di PDF. backend/laporan/ekspor.py:15

Laporankolom (kunci, label, jenis)
penjualanperiode/tanggal, jumlah_transaksi/angka, bruto/uang, retur/uang, neto/uang
produk terlarissku/teks, nama/teks, botol_terjual/angka, nilai/uang
stoksku/teks, nama/teks, stok_teks/teks, stok_botol/angka, stok_minimum/angka, stok_kedaluwarsa/angka, kedaluwarsa_terdekat/tanggal, status/teks
piutangnomor/teks, pelanggan/teks, tanggal/tanggal, jatuh_tempo/tanggal, total/uang, retur/uang, terbayar/uang, sisa/uang, terlambat_hari/angka

backend/laporan/views.py:72-78 backend/laporan/views.py:101-106 backend/laporan/views.py:123-132 backend/laporan/views.py:146-156

Untuk response JSON, field uang memakai helper uang() yang menghasilkan DecimalField(max_digits=18, decimal_places=2), sehingga angka uang terkirim sebagai string desimal seperti "14000.00". Field tanggal pada baris stok dan piutang dibuka dengan allow_null=True karena kedaluwarsa_terdekat dan jatuh_tempo boleh kosong. backend/laporan/serializers.py:4-13 backend/laporan/serializers.py:24-33 backend/laporan/serializers.py:36-46

Dua jalur berkas

Kedua jalur berkas memakai satu helper unduhan bernama _unduhan, yang memasang Content-Disposition: attachment pada HttpResponse. backend/laporan/ekspor.py:18-21

ke_xlsx menamai lembar kerja dengan judul[:31] karena batas panjang nama sheet, mencetak header tebal, dan mengubah Decimal menjadi float sebelum menulis sel. Nilai bertipe str selalu diberi data_type = "s" supaya teks kiriman pengguna tidak dieksekusi sebagai rumus Excel; komentar di kode menyebut alasan ini secara langsung. Format angka diatur lewat number_format #,##0.00 untuk uang dan dd/mm/yyyy untuk tanggal. Nama berkasnya {nama}.xlsx. backend/laporan/ekspor.py:24-49

jenisSel xlsxTeks PDF lewat _tampil
teksdata_type = "s"str(nilai)
angkanilai apa adanyastr(nilai)
uangnumber_format #,##0.00rupiah(nilai)
tanggalnumber_format dd/mm/yyyynilai.strftime("%d/%m/%Y")

_tampil mengembalikan string kosong untuk None, dan hanya kolom berjenis uang serta angka yang diberi penanda angka untuk perataan kanan. backend/laporan/ekspor.py:52-59

ke_pdf mengimpor weasyprint di dalam fungsi, merender laporan/tabel.html dengan konteks perusahaan dari settings.NAMA_PERUSAHAAN, judul, periode, dicetak dari timezone.localtime(), serta kepala dan baris yang sudah dipetakan ke bentuk siap tampil. Hasil HTML(string=html).write_pdf() dikirim sebagai application/pdf dengan nama {nama}.pdf. backend/laporan/ekspor.py:62-83

Template tabel.html mengatur halaman A4 landscape dan mencetak "Halaman X dari Y" di sudut kanan bawah. Header tabel diulang di setiap halaman lewat thead { display: table-header-group; }, dan tabel tanpa baris menampilkan teks Tidak ada data.. backend/laporan/templates/laporan/tabel.html:6 backend/laporan/templates/laporan/tabel.html:20-28

⚠️ PDF bergantung pada pustaka sistem GTK/Pango. Di Windows, PDF nota dan laporan tidak jalan tanpa pustaka itu, sementara fitur lain tetap normal dan image Docker sudah memuatnya. Test PDF melewati dirinya sendiri bila import weasyprint gagal dengan OSError, jadi kegagalan dependensi ini tidak terlihat sebagai test gagal. Versi pustaka yang dipatok adalah openpyxl==3.1.5 dan weasyprint==70.0. README.md:47 backend/requirements.txt:7-8 backend/tests/test_laporan.py:125-129

Izin dan batas modul

Semua laporan memakai izin LIHAT_LAPORAN yang nilainya string lihat_laporan. Di PunyaIzin, view tanpa deklarasi izin selalu ditolak, dan view dengan deklarasi hanya lolos bila string itu ada di pengguna.izin. Pada IZIN_PERAN, hanya peran pemilik yang memegang LIHAT_LAPORAN, karena admin_penjualan hanya mendapat LIHAT_PRODUK dan KELOLA_TRANSAKSI, sedangkan staf_gudang mendapat LIHAT_PRODUK dan KELOLA_STOK. backend/inti/izin.py:10 backend/inti/izin.py:31-35 backend/inti/izin.py:52-64

DasborView tinggal di modul yang sama tetapi memakai izin LIHAT_PRODUK dan mengisi field keuangan secara bertahap: blok KELOLA_TRANSAKSI mengisi hitungan transaksi hari ini dan tagihan jatuh tempo, blok LIHAT_LAPORAN mengisi penjualan_hari_ini, total_piutang, pembatalan_menunggu, tren, dan terlaris, sedangkan opname_menunggu menunggu SETUJUI_OPNAME. Field yang tidak terisi tetap bernilai None dan serializer membiarkannya null. Cara frontend memakai response ini dibahas di Frontend: Halaman dan Pengambilan Data. backend/laporan/views.py:166-172 backend/laporan/views.py:208-227

Yang dikunci test

TestYang dijaga
test_laporan_penjualan_neto_retur_dan_bataltransaksi batal tidak dihitung; bruto 14000.00, retur 3500.00, neto 10500.00; botol_terjual 3
test_laporan_per_bulanperiode sama dengan tanggal 1 bulan berjalan untuk per=bulan
test_periode_terbalik_ditolakstatus 400 saat dari melewati sampai
test_laporan_piutang_dan_terlambatsisa 7000.00 dan terlambat_hari 3
test_ekspor_excel_tidak_menjalankan_rumuscell(row=2, column=2).data_type == "s" untuk pelanggan bernama =HYPERLINK("http://x","y")
test_nota_dan_laporan_pdfresponse ekspor=pdf diawali %PDF, atau test di-skip bila Pango tidak ada

backend/tests/test_laporan.py:13-43 backend/tests/test_laporan.py:81-100 backend/tests/test_laporan.py:125-138