Membuat Cetak Nota Kasir Laravel 58mm dan 80mm
Diterbitkan 16 September 2026
Pada tahap kesepuluh kita akan membuat cetak nota kasir untuk transaksi penjualan.
Nota digunakan sebagai bukti transaksi dan biasanya dicetak menggunakan printer thermal.
Pada tutorial ini kita akan membuat dua ukuran umum:
58 mm
80 mm
Nota akan menampilkan:
nama toko
alamat toko
telepon toko
nomor invoice
tanggal transaksi
nama kasir
nama pelanggan
daftar produk
quantity
harga
subtotal
diskon
grand total
jumlah pembayaran
kembalian
footer nota
status transaksi
Data toko akan diambil dari tabel:
settings
yang sudah dibuat pada Tahap 03.
Kita juga akan membuat:
route khusus nota
view khusus nota
CSS print
tombol cetak
pilihan ukuran 58mm dan 80mm
Hasil yang Akan Dibuat
Pada tahap ini kita akan menambahkan method pada:
SaleController
Route:
/sales/{sale}/receipt
View:
resources/views/sales/receipt.blade.php
Fitur:
- tampil nota transaksi;
- ukuran 58 mm;
- ukuran 80 mm;
- tombol print;
- print-friendly CSS;
- informasi toko dari settings;
- transaksi cancelled diberi penanda;
- layout sederhana untuk printer thermal.
Persiapan
Pastikan Tahap 01 sampai Tahap 09 sudah selesai.
Aplikasi harus sudah mempunyai:
sales
sale_details
settings
products
users
customers
Pastikan transaksi penjualan sudah tersedia.
Pastikan juga settings dari seeder sudah ada:
store_name
store_address
store_phone
receipt_footer
1. Memeriksa Settings
Jalankan:
php artisan tinker
Cek:
App\Models\Setting::pluck('value', 'key');
Hasil kurang lebih:
store_name => Toko RumahSourceCode
store_address => Denpasar, Bali
store_phone => 081234567890
receipt_footer => Terima kasih telah berbelanja.
Keluar:
exit
2. Menambahkan Route Cetak Nota
Buka:
routes/web.php
Tambahkan route di group auth:
Route::middleware(['auth'])->group(function () {
Route::get(
'sales/{sale}/receipt',
[SaleController::class, 'receipt']
)->name('sales.receipt');
});
Admin dan kasir boleh membuka nota.
3. Menambahkan Method receipt pada SaleController
Pastikan import:
use App\Models\Setting;
Kemudian tambahkan method:
public function receipt(
Request $request,
Sale $sale
): View {
$size = $request->get('size', '58');
if (! in_array($size, ['58', '80'], true)) {
$size = '58';
}
$sale->load([
'user',
'customer',
'details.product.unit',
]);
$settings = Setting::query()
->pluck('value', 'key');
return view('sales.receipt', compact(
'sale',
'settings',
'size'
));
}
4. Penjelasan Parameter Size
URL dapat menggunakan:
?s=58
Namun pada controller kita menggunakan:
size
Contoh:
/sales/1/receipt?size=58
atau:
/sales/1/receipt?size=80
Jika value lain dikirim, controller mengembalikan default:
58
5. Mengambil Data Settings
Kode:
$settings = Setting::query()
->pluck('value', 'key');
menghasilkan collection key-value.
Contoh penggunaan di Blade:
{{ $settings['store_name'] ?? 'Nama Toko' }}
Jika key tidak tersedia, fallback tetap ditampilkan.
6. Menambahkan Tombol Nota pada Detail Penjualan
Buka:
resources/views/sales/show.blade.php
Tambahkan tombol:
<div class="d-flex gap-2 mb-3">
<a
href="{{ route('sales.receipt', [
'sale' => $sale,
'size' => 58,
]) }}"
target="_blank"
class="btn btn-outline-primary">
Cetak Nota 58mm
</a>
<a
href="{{ route('sales.receipt', [
'sale' => $sale,
'size' => 80,
]) }}"
target="_blank"
class="btn btn-outline-secondary">
Cetak Nota 80mm
</a>
</div>
Dengan:
target="_blank"
nota dibuka pada tab baru.
7. Membuat View Receipt
Buat:
resources/views/sales/receipt.blade.php
Isi:
<!DOCTYPE html>
<html lang="id">
<head>
<meta charset="UTF-8">
<meta
name="viewport"
content="width=device-width, initial-scale=1.0">
<title>
Nota {{ $sale->invoice_number }}
</title>
<style>
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 12px;
background: #f5f5f5;
font-family: Arial, Helvetica, sans-serif;
color: #000;
}
.receipt {
width: {{ $size === '80' ? '80mm' : '58mm' }};
margin: 0 auto;
background: #fff;
padding: {{ $size === '80' ? '4mm' : '3mm' }};
font-size: {{ $size === '80' ? '12px' : '10px' }};
line-height: 1.35;
}
.text-center {
text-align: center;
}
.text-right {
text-align: right;
}
.fw-bold {
font-weight: bold;
}
.store-name {
font-size: {{ $size === '80' ? '18px' : '14px' }};
font-weight: bold;
margin-bottom: 2px;
}
.separator {
border-top: 1px dashed #000;
margin: 6px 0;
}
.row {
display: flex;
justify-content: space-between;
gap: 8px;
}
.row > span:first-child {
flex: 1;
}
.row > span:last-child {
white-space: nowrap;
}
.item {
margin-bottom: 5px;
}
.item-name {
font-weight: bold;
}
.item-detail {
display: flex;
justify-content: space-between;
gap: 8px;
}
.status-cancelled {
border: 2px solid #000;
padding: 5px;
text-align: center;
font-weight: bold;
margin: 8px 0;
}
.toolbar {
max-width: 800px;
margin: 0 auto 12px;
display: flex;
gap: 8px;
justify-content: center;
flex-wrap: wrap;
}
.toolbar a,
.toolbar button {
border: 1px solid #333;
background: #fff;
padding: 8px 12px;
cursor: pointer;
text-decoration: none;
color: #000;
font-size: 14px;
}
@media print {
body {
padding: 0;
margin: 0;
background: #fff;
}
.toolbar {
display: none !important;
}
.receipt {
margin: 0;
width: {{ $size === '80' ? '80mm' : '58mm' }};
padding: {{ $size === '80' ? '4mm' : '3mm' }};
}
@page {
size: {{ $size === '80' ? '80mm' : '58mm' }} auto;
margin: 0;
}
}
</style>
</head>
<body>
<div class="toolbar">
<button
type="button"
onclick="window.print()">
Cetak
</button>
<a
href="{{ route('sales.receipt', [
'sale' => $sale,
'size' => 58,
]) }}">
58mm
</a>
<a
href="{{ route('sales.receipt', [
'sale' => $sale,
'size' => 80,
]) }}">
80mm
</a>
<a
href="{{ route('sales.show', $sale) }}">
Kembali
</a>
</div>
<div class="receipt">
<div class="text-center">
<div class="store-name">
{{ $settings['store_name'] ?? 'Nama Toko' }}
</div>
@if(! empty($settings['store_address']))
<div>
{{ $settings['store_address'] }}
</div>
@endif
@if(! empty($settings['store_phone']))
<div>
Telp: {{ $settings['store_phone'] }}
</div>
@endif
</div>
<div class="separator"></div>
<div class="row">
<span>Invoice</span>
<span>{{ $sale->invoice_number }}</span>
</div>
<div class="row">
<span>Tanggal</span>
<span>
{{ $sale->sale_date->format('d/m/Y H:i') }}
</span>
</div>
<div class="row">
<span>Kasir</span>
<span>{{ $sale->user?->name ?? '-' }}</span>
</div>
<div class="row">
<span>Pelanggan</span>
<span>{{ $sale->customer?->name ?? 'Umum' }}</span>
</div>
@if($sale->status === 'cancelled')
<div class="status-cancelled">
TRANSAKSI DIBATALKAN
</div>
@endif
<div class="separator"></div>
@foreach($sale->details as $detail)
<div class="item">
<div class="item-name">
{{ $detail->product?->name ?? 'Produk' }}
</div>
<div class="item-detail">
<span>
{{ $detail->quantity }}
x
{{ number_format(
(float) $detail->price,
0,
',',
'.'
) }}
</span>
<span>
{{ number_format(
(float) $detail->subtotal,
0,
',',
'.'
) }}
</span>
</div>
</div>
@endforeach
<div class="separator"></div>
<div class="row">
<span>Subtotal</span>
<span>
Rp {{ number_format(
(float) $sale->subtotal,
0,
',',
'.'
) }}
</span>
</div>
<div class="row">
<span>Diskon</span>
<span>
Rp {{ number_format(
(float) $sale->discount,
0,
',',
'.'
) }}
</span>
</div>
<div class="row fw-bold">
<span>Grand Total</span>
<span>
Rp {{ number_format(
(float) $sale->grand_total,
0,
',',
'.'
) }}
</span>
</div>
<div class="row">
<span>Dibayar</span>
<span>
Rp {{ number_format(
(float) $sale->paid_amount,
0,
',',
'.'
) }}
</span>
</div>
<div class="row">
<span>Kembalian</span>
<span>
Rp {{ number_format(
(float) $sale->change_amount,
0,
',',
'.'
) }}
</span>
</div>
@if($sale->notes)
<div class="separator"></div>
<div>
<strong>Catatan:</strong>
</div>
<div>
{!! nl2br(e($sale->notes)) !!}
</div>
@endif
<div class="separator"></div>
<div class="text-center">
{{ $settings['receipt_footer']
?? 'Terima kasih telah berbelanja.' }}
</div>
</div>
</body>
</html>
8. Mengapa Receipt Tidak Menggunakan Layout Utama?
Halaman nota menggunakan HTML mandiri.
Kita tidak memakai:
@extends('layouts.app')
karena layout aplikasi biasanya memiliki:
navbar
sidebar
container
margin
padding
footer
Semua elemen tersebut tidak dibutuhkan pada printer thermal.
Nota sebaiknya minimal dan fokus pada isi transaksi.
9. Ukuran 58mm
Ukuran:
58mm
cocok untuk printer thermal kecil.
CSS:
.receipt {
width: 58mm;
}
dan:
@page {
size: 58mm auto;
}
10. Ukuran 80mm
Untuk printer 80mm:
width: 80mm;
Ukuran font juga dibuat sedikit lebih besar.
11. Mengapa Ada Toolbar?
Toolbar hanya tampil di browser.
Isi:
Cetak
58mm
80mm
Kembali
Saat print:
.toolbar {
display: none !important;
}
Jadi tombol tidak ikut tercetak.
12. Tombol Print
Tombol:
<button onclick="window.print()">
membuka print dialog browser.
Kasir dapat memilih printer thermal yang terhubung ke komputer.
13. Auto Print Opsional
Jika ingin print dialog langsung muncul ketika halaman dibuka, tambahkan sebelum:
</body>
kode:
<script>
window.addEventListener('load', () => {
window.print();
});
</script>
Namun tutorial ini tidak mengaktifkannya secara default.
Alasannya:
- pengguna mungkin hanya ingin preview;
- browser dapat membatasi auto print;
- lebih nyaman jika kasir memilih tombol Cetak.
14. Informasi Toko
Nama toko:
{{ $settings['store_name'] ?? 'Nama Toko' }}
Alamat:
{{ $settings['store_address'] }}
Telepon:
{{ $settings['store_phone'] }}
Footer:
{{ $settings['receipt_footer'] }}
15. Jika Setting Tidak Tersedia
Kita menggunakan fallback.
Contoh:
$settings['store_name'] ?? 'Nama Toko'
Jadi halaman tidak error jika setting belum tersedia.
16. Menampilkan Status Cancelled
Jika:
status = cancelled
nota menampilkan:
TRANSAKSI DIBATALKAN
Ini penting agar nota transaksi yang sudah dibatalkan tidak terlihat seperti transaksi aktif.
17. Apakah Nota Cancelled Boleh Dicetak?
Secara teknis boleh.
Namun nota harus memiliki tanda jelas bahwa transaksi:
DIBATALKAN
Hal ini berguna untuk:
audit
arsip
pengecekan transaksi lama
18. Menampilkan Detail Produk
Setiap item menampilkan:
nama produk
quantity
harga
subtotal
Contoh:
Air Mineral
2 x 4.000 8.000
19. Harga pada Nota Berasal dari SaleDetail
Nota menggunakan:
$detail->price
bukan:
$product->selling_price
Alasannya harga produk saat ini bisa berubah.
Nota harus menggunakan harga saat transaksi terjadi.
20. Subtotal pada Nota
Subtotal item menggunakan:
$detail->subtotal
Data ini sudah disimpan saat transaksi dibuat.
21. Ringkasan Pembayaran
Nota menampilkan:
Subtotal
Diskon
Grand Total
Dibayar
Kembalian
Semua berasal dari header:
sales
22. Memformat Rupiah
Kita menggunakan:
number_format(
(float) $value,
0,
',',
'.'
)
Contoh:
50000
menjadi:
50.000
23. Mengapa Tidak Menampilkan Rp pada Setiap Item?
Pada ukuran thermal kecil, ruang terbatas.
Karena itu harga item dapat ditampilkan:
4.000
tanpa:
Rp
Sedangkan total utama tetap menggunakan:
Rp
24. Menguji Nota 58mm
Buka salah satu transaksi.
Klik:
Cetak Nota 58mm
Pastikan:
lebar nota sempit
teks terbaca
item tidak keluar area
total terlihat jelas
toolbar tidak ikut print
25. Menguji Nota 80mm
Klik:
Cetak Nota 80mm
Pastikan tampilan lebih lebar.
26. Menguji Print Preview
Klik:
Cetak
Browser membuka print preview.
Pada print preview:
toolbar tidak tampil
background putih
nota berada pada lebar thermal
27. Printer Thermal dan Browser
Aplikasi Laravel hanya menghasilkan halaman HTML.
Proses print dilakukan browser.
Artinya Laravel tidak langsung berbicara ke printer thermal.
Browser mengirim output ke printer yang dipilih pengguna.
28. Jika Hasil Print Terpotong
Periksa:
paper size printer
margin printer
scale browser
Gunakan:
Scale 100%
Margin None
jika browser menyediakan opsi tersebut.
Printer juga harus menggunakan ukuran kertas sesuai:
58mm
atau:
80mm
29. Jika Printer 58mm Memiliki Area Cetak Lebih Kecil
Beberapa printer 58mm tidak benar-benar memiliki area cetak penuh 58mm.
Jika teks terlalu dekat pinggir, ubah:
padding: 3mm;
menjadi:
padding: 2mm;
atau kecilkan:
font-size
30. Jika Printer 80mm
Untuk 80mm, biasanya area cetak lebih luas.
Layout dapat menampilkan font sedikit lebih besar.
31. Mengapa Tidak Menggunakan Bootstrap?
Untuk halaman thermal, CSS manual lebih ringan.
Bootstrap dapat membawa:
margin
padding
font
print style
yang tidak kita perlukan.
32. Mengapa Tidak Menggunakan PDF?
Untuk printer thermal, HTML print sering lebih praktis.
PDF dapat digunakan jika:
nota perlu diunduh
nota perlu dikirim
arsip PDF dibutuhkan
Namun PDF bukan kebutuhan wajib untuk tutorial dasar.
33. Menambahkan Tombol Nota pada Daftar Penjualan
Opsional, pada:
resources/views/sales/index.blade.php
tambahkan tombol:
<a
href="{{ route('sales.receipt', [
'sale' => $sale,
'size' => 58,
]) }}"
target="_blank"
class="btn btn-sm btn-outline-secondary">
Nota
</a>
Dengan demikian kasir tidak perlu membuka detail lebih dahulu.
34. Menambahkan Pengaturan Nota ke CRUD Setting
Saat ini settings berasal dari seeder.
Jika ingin membuat halaman admin:
Pengaturan Toko
dapat dibuat CRUD khusus.
Namun untuk menjaga tutorial tetap fokus, tahap ini hanya membaca setting yang sudah tersedia.
35. Setting yang Disarankan
Minimal:
store_name
store_address
store_phone
receipt_footer
Opsional:
store_email
store_tax_number
receipt_header
receipt_footer_2
36. Menambahkan Nomor WhatsApp Opsional
Jika ingin menampilkan WhatsApp:
store_whatsapp
buat melalui data setting.
Namun belum diperlukan.
37. Menambahkan Logo pada Nota
Logo dapat digunakan, tetapi pada printer thermal:
- hasil dapat kurang tajam;
- printing lebih lambat;
- file lebih besar;
- printer murah kadang kurang bagus mencetak gambar.
Untuk tutorial dasar kita gunakan nota teks.
38. Menambahkan QR Code Opsional
QR code dapat ditambahkan untuk:
link invoice
pembayaran
WhatsApp
review toko
Namun membutuhkan library tambahan.
Belum diperlukan untuk fungsi utama kasir.
39. Menguji Transaksi Cancelled
Buka transaksi cancelled.
Cetak nota.
Pastikan teks:
TRANSAKSI DIBATALKAN
terlihat jelas.
40. Menguji Pelanggan Umum
Jika:
customer_id = NULL
nota menampilkan:
Pelanggan: Umum
41. Menguji Produk dengan Unit NULL
View menggunakan:
$detail->product?->unit
jadi jika unit tidak tersedia, nota tidak error.
42. Menguji Produk Lama
Karena produk yang sudah mempunyai transaksi tidak boleh dihapus, relasi produk biasanya tetap tersedia.
Namun null-safe operator tetap digunakan agar view aman.
43. Keamanan Nota
Route menggunakan:
auth
Jadi orang yang belum login tidak dapat membuka nota.
Jika dibutuhkan kontrol lebih ketat, dapat dibuat policy.
Untuk tutorial ini auth sudah cukup.
44. Apakah Kasir Bisa Cetak Nota Transaksi Kasir Lain?
Pada tutorial ini, semua user login dapat membuka transaksi.
Jika bisnis mengharuskan kasir hanya melihat transaksi sendiri, perlu policy atau query tambahan.
Belum diterapkan pada tutorial dasar.
45. Struktur File Setelah Tahap 10
Controller:
app/Http/Controllers/SaleController.php
Method tambahan:
receipt()
View:
resources/views/sales/receipt.blade.php
Route:
sales.receipt
46. Checklist Pengujian Tahap 10
Pastikan:
[ ] route receipt dapat dibuka
[ ] nota memuat nama toko
[ ] alamat tampil
[ ] telepon tampil
[ ] invoice tampil
[ ] tanggal tampil
[ ] kasir tampil
[ ] pelanggan tampil
[ ] item tampil
[ ] quantity tampil
[ ] harga tampil
[ ] subtotal tampil
[ ] diskon tampil
[ ] grand total tampil
[ ] pembayaran tampil
[ ] kembalian tampil
[ ] footer tampil
Ukuran:
[ ] 58mm bekerja
[ ] 80mm bekerja
[ ] tombol ukuran bekerja
Print:
[ ] tombol Cetak membuka print dialog
[ ] toolbar tidak ikut tercetak
[ ] lebar nota sesuai
[ ] margin tidak berlebihan
Status:
[ ] transaksi paid tampil normal
[ ] transaksi cancelled diberi tanda
47. Kesalahan Umum: Menggunakan Selling Price Produk
Jangan gunakan:
$product->selling_price
untuk nota transaksi lama.
Gunakan:
$detail->price
48. Kesalahan Umum: Sidebar Ikut Tercetak
Gunakan halaman receipt mandiri.
Jangan gunakan layout aplikasi utama jika sidebar ikut terbawa.
49. Kesalahan Umum: Tombol Ikut Tercetak
Pastikan:
@media print {
.toolbar {
display: none !important;
}
}
50. Kesalahan Umum: Nota Terlalu Lebar
Pastikan ukuran CSS:
58mm
80mm
sesuai printer.
Jangan menggunakan:
width: 100%
tanpa batas.
51. Kesalahan Umum: Margin Browser Besar
Pada print dialog, pilih:
Margins: None
jika tersedia.
52. Kesalahan Umum: Scale Tidak 100%
Gunakan:
Scale: 100%
agar ukuran tidak berubah.
53. Kesalahan Umum: Nota Cancelled Tidak Dibedakan
Selalu tampilkan status cancelled dengan jelas.
54. Pengembangan Selanjutnya
Nota dapat dikembangkan dengan:
logo
QR code
barcode invoice
auto print
print Bluetooth
print ESC/POS
print via Android
download PDF
kirim WhatsApp
email receipt
Fitur tersebut merupakan pengembangan lanjutan.
55. Checkpoint Hasil
Setelah Tahap 10:
Login : selesai
Role admin/kasir : selesai
Database : selesai
Model dan relasi : selesai
Seeder : selesai
CRUD master : selesai
CRUD produk : selesai
Penjualan : selesai
Pembelian : selesai
Kartu stok : selesai
Adjustment stok : selesai
Dashboard : selesai
Laporan : selesai
Nota 58mm : selesai
Nota 80mm : selesai
Testing : tahap berikutnya
Deploy : belum
56. Ringkasan
Pada tahap kesepuluh kita telah membuat nota kasir untuk printer thermal.
Nota mendukung:
58mm
80mm
Data toko diambil dari:
settings
Data transaksi diambil dari:
sales
sale_details
Harga item menggunakan snapshot:
sale_details.price
bukan harga produk saat ini.
Nota cancelled diberi penanda jelas:
TRANSAKSI DIBATALKAN
Print dilakukan menggunakan:
window.print()
dan CSS:
@media print
Tahap berikutnya kita akan membuat pengujian sistem kasir secara khusus sebelum masuk ke tahap deployment.




