Lewati ke konten
Skematik

Cara Membuat Aplikasi Kasir Sederhana di Google Spreadsheet + Apps Script

7 menit baca · Diperbarui 2026-07-22

Aplikasi kasir tidak harus beli lisensi atau install apapun. Kalau kamu sudah punya data barang di Google Spreadsheet, kamu tinggal menempelkan lapisan Apps Script di atasnya dan jadilah kasir yang bisa dibuka dari HP lewat browser.

Yang kita bangun di sini bukan mockup. Ini Web App beneran: buka link di HP, pilih barang, masukin uang yang dibayar, angka kembalian keluar, tekan simpan, transaksi masuk ke sheet. Di akhir hari ada rekap omzet. Semua kode di bawah bisa langsung kamu tempel dan jalan.

Apa yang kita bangun

Alurnya sederhana dan itu memang tujuannya. Satu Spreadsheet jadi database, satu file HTML jadi tampilan kasir, beberapa fungsi Apps Script jadi jembatan di antaranya.

  • Daftar produk tampil di HP, tinggal tap untuk masuk keranjang
  • Total dan kembalian dihitung otomatis begitu kamu ketik uang yang dibayar
  • Tekan simpan, satu baris transaksi masuk ke sheet lengkap dengan waktu dan ID
  • Rekap harian menjumlahkan jumlah transaksi dan omzet, bisa dijadwalkan tiap malam

Kalau kamu baru pertama menyentuh Apps Script, baca dulu Google Apps Script untuk pemula dan cara membuat Web App dari Google Spreadsheet supaya konsep doGet dan deploy tidak terasa asing. Di sini kita fokus ke kasusnya.

Struktur sheet dulu, kode belakangan

Di aplikasi Apps Script, struktur tab dan kolom Spreadsheet itu skema database kamu. Rancang ini benar dari awal, sisanya jadi gampang. Buat satu Spreadsheet dengan tiga tab.

  • Tab Produk dengan kolom: SKU, Nama, Harga. Ini katalog yang muncul di layar kasir.
  • Tab Transaksi dengan kolom: Waktu, ID, Item, Total, Bayar, Kembalian. Setiap penjualan jadi satu baris di sini.
  • Tab Rekap dengan kolom: Tanggal, Jumlah Transaksi, Omzet. Diisi oleh fungsi rekap.

Backend: baca produk dan simpan transaksi

Buka Extensions lalu Apps Script dari Spreadsheet-mu. Fungsi getProduk membaca seluruh katalog sekali jalan pakai getValues(), bukan getValue() dipanggil berulang di dalam loop yang bikin lambat. Fungsi simpanTransaksi menulis satu baris dan dibungkus LockService supaya dua kasir yang menekan simpan berbarengan tidak saling menimpa baris.

// Code.gs (server Apps Script)
const SHEET_PRODUK = 'Produk';
const SHEET_TRX = 'Transaksi';

// Sajikan halaman kasir sebagai Web App
function doGet() {
  return HtmlService.createHtmlOutputFromFile('Index')
    .setTitle('Kasir')
    .addMetaTag('viewport', 'width=device-width, initial-scale=1');
}

// Baca semua produk sekali jalan (bulk read, bukan getValue per sel di loop)
function getProduk() {
  const sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_PRODUK);
  const rows = sheet.getDataRange().getValues();
  rows.shift(); // buang baris header
  return rows
    .filter(function (r) { return r[0] !== ''; })
    .map(function (r) {
      return { sku: String(r[0]), nama: String(r[1]), harga: Number(r[2]) };
    });
}

// Simpan satu transaksi. Total dihitung ulang di server biar tidak bisa diakali dari HP.
function simpanTransaksi(data) {
  const lock = LockService.getScriptLock();
  lock.waitLock(30000); // antre maksimal 30 detik kalau ada kasir lain menyimpan
  try {
    const sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_TRX);
    const waktu = new Date();
    const id = 'TRX' + waktu.getTime();

    const total = data.items.reduce(function (sum, it) {
      return sum + Number(it.harga) * Number(it.qty);
    }, 0);
    const bayar = Number(data.bayar) || 0;
    const kembalian = bayar - total;
    const ringkasan = data.items.map(function (it) {
      return it.nama + ' x' + it.qty;
    }).join(', ');

    // Tulis satu baris utuh, bukan sel per sel
    sheet.appendRow([waktu, id, ringkasan, total, bayar, kembalian]);

    return { ok: true, id: id, total: total, kembalian: kembalian };
  } finally {
    lock.releaseLock(); // selalu lepas lock walau ada error
  }
}

Perhatikan total dihitung ulang di server dari harga dan qty, bukan menerima angka total mentah dari HP. Ini menutup celah kalau ada yang iseng mengubah nilai lewat browser.

Frontend: halaman kasir di HP

Buat file HTML baru bernama Index (ikon plus di sebelah Files, pilih HTML). Kunci di sini: halaman client memanggil fungsi server lewat google.script.run, bukan fetch. fetch tidak akan bekerja ke fungsi Apps Script. Total dan kembalian dihitung di sisi client biar responsif, lalu diverifikasi ulang saat disimpan.

<!DOCTYPE html>
<html>
<head>
  <base target="_top">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    body { font-family: system-ui, sans-serif; margin: 0; padding: 16px; }
    .item { display: flex; justify-content: space-between; align-items: center;
            padding: 8px 0; border-bottom: 1px solid #eee; }
    button { padding: 12px; font-size: 16px; }
    #simpan { width: 100%; }
    input { padding: 12px; font-size: 16px; width: 100%; box-sizing: border-box; }
    .total { font-size: 20px; font-weight: bold; margin: 12px 0; }
  </style>
</head>
<body>
  <h2>Kasir</h2>
  <div id="daftar">Memuat produk...</div>
  <div class="total">Total: Rp <span id="total">0</span></div>
  <input id="bayar" type="number" inputmode="numeric" placeholder="Uang dibayar">
  <div class="total">Kembalian: Rp <span id="kembalian">0</span></div>
  <button id="simpan" onclick="simpan()">Simpan Transaksi</button>

  <script>
    let keranjang = {}; // sku -> { nama, harga, qty }

    function rupiah(n) { return Number(n).toLocaleString('id-ID'); }

    function hitungTotal() {
      let total = 0;
      for (const sku in keranjang) {
        total += keranjang[sku].harga * keranjang[sku].qty;
      }
      document.getElementById('total').textContent = rupiah(total);
      hitungKembalian(total);
      return total;
    }

    function hitungKembalian(total) {
      const bayar = Number(document.getElementById('bayar').value) || 0;
      const kembalian = bayar - total;
      document.getElementById('kembalian').textContent = rupiah(kembalian < 0 ? 0 : kembalian);
    }

    document.getElementById('bayar').addEventListener('input', function () {
      hitungKembalian(hitungTotal());
    });

    function tambah(sku, nama, harga) {
      if (!keranjang[sku]) keranjang[sku] = { nama: nama, harga: harga, qty: 0 };
      keranjang[sku].qty++;
      hitungTotal();
    }

    function render(produk) {
      const div = document.getElementById('daftar');
      div.innerHTML = '';
      produk.forEach(function (p) {
        const row = document.createElement('div');
        row.className = 'item';
        row.innerHTML = '<span>' + p.nama + ' - Rp ' + rupiah(p.harga) + '</span>';
        const btn = document.createElement('button');
        btn.textContent = '+';
        btn.onclick = function () { tambah(p.sku, p.nama, p.harga); };
        row.appendChild(btn);
        div.appendChild(row);
      });
    }

    function simpan() {
      const bayar = Number(document.getElementById('bayar').value) || 0;
      const items = Object.keys(keranjang).map(function (sku) {
        return { nama: keranjang[sku].nama, harga: keranjang[sku].harga, qty: keranjang[sku].qty };
      });
      if (items.length === 0) { alert('Keranjang masih kosong'); return; }

      google.script.run
        .withSuccessHandler(function (res) {
          alert('Tersimpan. ID ' + res.id + '. Kembalian Rp ' + rupiah(res.kembalian));
          keranjang = {};
          document.getElementById('bayar').value = '';
          hitungTotal();
        })
        .withFailureHandler(function (err) {
          alert('Gagal menyimpan: ' + err.message);
        })
        .simpanTransaksi({ items: items, bayar: bayar });
    }

    // Muat produk saat halaman dibuka
    google.script.run.withSuccessHandler(render).getProduk();
  </script>
</body>
</html>

Layar ini sengaja dibuat sederhana dengan tombol besar supaya nyaman dipakai satu tangan di HP. Kalau kamu butuh pola input yang lebih rapi dari ponsel, ada bahasannya di form input data ke Spreadsheet dari HP.

Rekap harian otomatis

Rekap tinggal membaca tab Transaksi sekali, menyaring baris yang tanggalnya hari ini, lalu menjumlahkan. Karena semua diproses di memori dari hasil getValues(), ini tetap cepat walau transaksi sudah ratusan baris.

// Code.gs (lanjutan)
function rekapHariIni() {
  const trx = SpreadsheetApp.getActive().getSheetByName(SHEET_TRX);
  const rows = trx.getDataRange().getValues();
  rows.shift(); // buang header

  const now = new Date();
  const y = now.getFullYear(), m = now.getMonth(), d = now.getDate();

  let jumlah = 0, omzet = 0;
  rows.forEach(function (r) {
    const waktu = r[0]; // kolom Waktu
    if (waktu instanceof Date &&
        waktu.getFullYear() === y && waktu.getMonth() === m && waktu.getDate() === d) {
      jumlah++;
      omzet += Number(r[3]); // kolom Total
    }
  });

  const rekap = SpreadsheetApp.getActive().getSheetByName('Rekap');
  rekap.appendRow([now, jumlah, omzet]);

  return { tanggal: now, jumlah: jumlah, omzet: omzet };
}

Jalankan manual dari editor untuk mengetes, atau pasang time-driven trigger supaya otomatis jalan tiap malam. Cara memasang jadwalnya ada di menjadwalkan script dengan trigger.

Deploy jadi Web App

Kode tidak berguna sampai di-deploy jadi Web App yang bisa dibuka di HP. Dari editor Apps Script:

  1. Klik Deploy lalu New deployment
  2. Pilih tipe Web app
  3. Execute as: Me (biar script punya akses ke Spreadsheet-mu)
  4. Who has access: Anyone
  5. Klik Deploy, salin URL Web App-nya, buka di browser HP

Mau langsung jadi tanpa mulai dari nol

Contoh di atas fondasinya utuh, tapi masih polos. Kalau kamu ingin versi yang sudah rapi dengan katalog, keranjang, struk, dan rekap terpasang, template KasirSheet sudah menyiapkannya. Tinggal salin, isi produkmu, deploy.

Lihat contoh PRD Web GAS (KasirSheet) →

Kalau butuh fitur di luar contoh ini

Begitu kebutuhan naik, potong stok otomatis tiap penjualan, cetak struk thermal, banyak kasir dengan hak akses berbeda, laporan per shift, kode di atas akan bercabang cepat. Di titik ini, menjelaskan maunya ke AI coding agent lebih hemat waktu daripada menambal manual. Tapi agent butuh instruksi jelas soal skema tab, daftar fungsi server, dan batasan khas Apps Script, bukan sekadar 'buatkan aplikasi kasir'.

Itu peran PRD. Skematik mode Web GAS menyusun PRD yang sudah memetakan struktur tab jadi skema data, daftar fungsi server yang perlu dibuat, plus konvensi sadar-batasan GAS, dan bisa di-export jadi setup-sheets.gs. Kalau PRD masih terdengar asing, mulai dari apa itu PRD, cara membuat PRD untuk aplikasi, dan cara menjelaskan ide aplikasi ke AI.

Buat PRD Web GAS gratis di Skematik →

Mulai dari contoh polos ini untuk paham cara kerjanya, lalu naik ke template atau PRD saat kebutuhan kasirmu makin serius. Yang penting kamu sudah punya kasir yang jalan hari ini, bukan bulan depan.

Bikin Aplikasi Kasir di Google Spreadsheet — Skematik