Cara Membuat Kasir Laundry dengan Google Apps Script: Harga Aman di Server, Nomor Nota Urut, dan Lacak Cucian
Banyak laundry kiloan masih mengandalkan buku nota rangkap dan grup WhatsApp. Masalahnya muncul pelan-pelan: nota hilang, harga ditulis beda antar kasir, pelanggan bolak-balik bertanya "sudah jadi belum?", dan di akhir bulan tidak ada yang tahu berapa uang yang benar-benar masuk.
Di tutorial ini kita membangun inti kasir laundry dengan Google Apps Script dan Google Sheets: perhitungan harga yang tidak bisa diakali dari browser, nomor nota urut per hari, alur status cucian, pembayaran bertahap, dan halaman lacak cucian yang aman. Contohnya diambil dari aplikasi Kasir Laundry Kiloan yang bisa kamu download gratis di Papicode (link di akhir artikel).
1. Hitung harga di server, bukan di browser
Halaman kasir boleh menampilkan subtotal secara langsung supaya terasa cepat, tetapi angka yang disimpan harus dihitung ulang di server. Kalau tidak, siapa pun yang membuka DevTools bisa mengirim harga Rp1 per kilo.
function hitung_(items, cfg) {
var lay = {};
bacaLayanan_().forEach(function (l) { lay[l.kode] = l; });
var minKg = Number(cfg.minimal_kg) || 0, out = [], jam = 0;
items.forEach(function (it) {
var l = lay[String(it.kode || '').toUpperCase()];
if (!l) throw new Error('Layanan tidak tersedia.');
var q = Number(it.qty);
if (l.satuan === 'kg') {
q = Math.round(q * 10) / 10; // timbangan 0,1 kg
if (!(q > 0 && q <= 100)) throw new Error('Berat harus 0,1 - 100 kg.');
} else if (!(q >= 1 && q % 1 === 0)) throw new Error();
tagih = l. === ? .(q, minKg) : q;
out.({ : l., : l., : q, : tagih, : l., : .(tagih * l.) });
jam = .(jam, l.);
});
{ : out, : out.( () { a + i.; }, ), : jam };
}
Perhatikan bahwa yang dikirim browser hanya kode dan qty. Harga diambil dari sheet Layanan, jadi saat pemilik menaikkan harga cukup ubah satu sel tanpa deploy ulang. Kita juga menyimpan qty (berat asli) dan tagih (berat yang ditagih) secara terpisah. Di struk, pelanggan bisa melihat bahwa cuciannya 1,2 kg tetapi ditagih 2 kg sesuai aturan minimal.
Estimasi selesai diambil dari durasi layanan terlama. Nota berisi cuci kering setrika (2 hari) dan cuci sepatu (3 hari) baru bisa diambil setelah 3 hari, karena pelanggan biasanya mengambil semua sekaligus.
2. Nomor nota urut per hari tanpa bentrok
Format LD-261020-007 mudah dibaca: tanggal plus urutan ke berapa hari itu. Tantangannya ada saat dua kasir menyimpan nota di detik yang sama. Keduanya membaca "terakhir 006" lalu sama-sama menulis 007. Solusinya, baca dan tulis di dalam kunci:
var lock = LockService.getScriptLock();
lock.waitLock(20000);
try {
var awalan = 'LD-' + Utilities.formatDate(new Date(), 'Asia/Jakarta', 'yyMMdd') + '-';
var urut = bacaOrder_()
.filter(function (o) { return o.nota.indexOf(awalan) === 0; })
.reduce(function (a, o) { return Math.max(a, Number(o.nota.slice(-3)) || 0); }, 0) + 1;
var nota = awalan + ('00' + urut).slice(-3);
sh.getRange(sh.getLastRow() + 1, 1, 1, KOL.length).setNumberFormat('@').setValues([baris]);
SpreadsheetApp.flush();
} {
lock.();
}
Dua detail kecil yang penting. Pertama, pakai Math.max dari nomor yang ada, bukan jumlah baris, supaya nomor tetap benar walaupun ada baris yang dihapus. Kedua, setNumberFormat('@') sebelum menulis. Tanpa itu Google Sheets bisa mengubah nomor HP 6285712349876 menjadi angka berformat ilmiah, dan tanggal-jam menjadi objek Date yang zona waktunya membingungkan.
3. Status yang hanya boleh maju
Cucian laundry punya alur yang jelas. Kita simpan sebagai array, lalu cukup bandingkan indeksnya:
var STATUS = ['Diterima', 'Dicuci', 'Dikeringkan', 'Disetrika', 'Siap Diambil', 'Diambil'];
function setStatus(token, nota, status) {
cek_(token);
var ke = STATUS.indexOf(status), o = cariNota_(nota);
if (ke <= STATUS.indexOf(o.status)) throw new Error('Nota ini sudah berstatus ' + o.status + '.');
if (status === 'Diambil' && o.dibayar < o.total)
throw new Error('Belum lunas. Sisa tagihan ' + rp_(o.total - o.dibayar) + '.');
sh.getRange(o.baris, 11).setValue(status);
sh.getRange(o.baris, 15).setValue(o.riwayat + '|' + status + '@' + sekarang_());
}
Kolom Riwayat menyimpan jejak seperti Diterima@2026-10-20 08:10|Dicuci@2026-10-20 11:02. Dari satu kolom teks ini halaman pelanggan bisa menggambar timeline lengkap dengan jamnya, tanpa sheet tambahan. Larangan mengambil cucian sebelum lunas menutup celah paling umum di laundry: cucian dibawa pulang, tagihannya terlupakan.
4. Pembayaran bertahap dicatat terpisah
Pelanggan sering membayar DP saat menaruh cucian, lalu melunasi saat mengambil. Kalau kita hanya menyimpan kolom "Dibayar" di nota, laporan kas harian jadi salah karena uang DP minggu lalu ikut terhitung hari ini. Karena itu setiap pembayaran ditulis sebagai baris baru di sheet Pembayaran:
function bayar(token, nota, jumlah, metode) {
cek_(token);
var j = Math.round(Number(jumlah) || 0), o = cariNota_(nota), sisa = o.total - o.dibayar;
if (sisa <= 0) throw new Error('Nota ini sudah lunas.');
if (j <= 0 || j > sisa) throw new Error('Pembayaran melebihi sisa tagihan.');
sh.getRange(o.baris, 10).setValue(String(o.dibayar + j)); // total dibayar di nota
catatBayar_(nota, j, metode, sekarang_()); // satu baris per transaksi kas
}
Hasilnya, laporan bisa menjawab dua pertanyaan berbeda: omzet (nilai nota yang masuk pada periode itu) dan kas masuk (uang yang benar-benar diterima, per Tunai / Transfer / QRIS). Piutang cukup dihitung dari selisih total dan dibayar di semua nota.
5. Halaman lacak yang tidak membocorkan data
Link lacak dikirim ke WhatsApp pelanggan, jadi anggap saja link itu bisa tersebar. Nomor nota mudah ditebak karena urut, sehingga kita minta bukti kedua yang hanya diketahui pemilik: 4 digit terakhir nomor HP.
function lacak(nota, empat) {
var cache = CacheService.getScriptCache(), kc = 'lacak_' + nota, n = Number(cache.get(kc) || 0);
if (n >= 6) throw new Error('Terlalu banyak percobaan. Coba lagi 15 menit lagi.');
var o = cariNota_(nota);
if (!o || o.hp.slice(-4) !== empat) { cache.put(kc, String(n + 1), 900); throw new Error('Nota atau 4 digit HP tidak cocok.'); }
return { nota: o.nota, nama: sensorNama_(o.nama), status: o.status, total: o.total, sisa: o.total - o.dibayar /* tanpa nomor HP */ };
}
Empat digit berarti 10.000 kemungkinan, dan dengan batas 6 percobaan per 15 menit menebak satu nota butuh waktu berminggu-minggu. Nama pelanggan disensor menjadi "Rina W." dan nomor HP tidak pernah ikut dikirim ke browser.
Ringkasan
- Browser hanya mengirim kode layanan dan jumlah; harga, minimal kg, dan estimasi dihitung server.
- Nomor nota urut per hari dibuat di dalam
LockServicedenganflush()sebelum kunci dilepas. - Status disimpan sebagai array sehingga aturan "hanya maju" cukup membandingkan indeks.
- Riwayat dalam satu kolom teks sudah cukup untuk menggambar timeline.
- Pembayaran dicatat per transaksi agar omzet, kas masuk, dan piutang bisa dibedakan.
- Lacak publik butuh faktor kedua, dibatasi percobaannya, dan tidak mengirim data pribadi.
Download source code lengkapnya
Versi lengkapnya sudah termasuk papan cucian per tahap, struk 58 mm, kirim nota ke WhatsApp, daftar harga dan kalkulator untuk pelanggan, form antar-jemput, pencarian nota, laporan omzet dengan grafik, pelanggan setia, dan ekspor CSV. Semua berjalan di Google Sheets tanpa hosting.
Source code lengkapnya gratis
Lihat semua yang dibahas di artikel ini langsung berjalan.
Tutorial lainnya
Cara Membuat Sistem Tiket Travel dengan Pilih Kursi di Google Apps Script: Layout Armada, Anti Kursi Dobel, dan Kursi Kedaluwarsa
20 Oktober 2026
Cara Membuat Website Donasi Transparan dengan Google Apps Script: Kode Unik Transfer dan Laporan Penyaluran
20 Oktober 2026