Ringkasan Utama
Membuat PDF invoice Indonesia berarti format Rupiah dengan titik sebagai pemisah ribuan, NPWP yang tercantum di dokumen, dan PPN yang dihitung pada tarif berlaku. Merender HTML lewat headless Chromium lebih baik daripada pustaka PDF berbasis koordinat, karena tata letak invoice adalah persoalan dokumen, bukan persoalan menggambar.
Membuat PDF faktur terdengar seperti masalah yang sudah selesai—ada selusin library untuk itu. Tapi dalam konteks bisnis Indonesia, persyaratannya berlapis: format Rupiah, NPWP, perhitungan PPN (12% atas DPP 11/12 untuk sebagian besar barang sejak 2025), dan tata letak merek yang tepat. Setelah mengevaluasi berbagai library, saya memilih Puppeteer—render HTML dengan kontrol CSS penuh, lalu print ke PDF.
Kenapa Puppeteer daripada Library PDF?
Library PDF murni memerlukan matematika koordinat manual untuk posisi, font, border tabel, dan perataan kolom. Dengan Puppeteer, Anda menulis faktur sebagai template HTML menggunakan CSS yang familiar: flexbox, CSS Grid, font loading standar, dan print media query untuk page break.
Desain Template dengan Handlebars
Handlebars adalah template engine karena logikanya ringan dan mendukung fungsi helper. Template invoice.hbs merender header perusahaan dengan logo tertanam sebagai base64, alamat pelanggan, tabel item baris, section total dengan PPN, dan footer dengan syarat pembayaran.
Format Rupiah Indonesia dan PPN
API Intl.NumberFormat dengan locale 'id-ID' dan mata uang 'IDR' menghasilkan format Rupiah yang benar: titik sebagai pemisah ribuan, tanpa desimal untuk nilai bulat (Rp 1.500.000). Sejak 1 Januari 2025, PMK 131/2024 menetapkan PPN 12%: untuk barang selain barang mewah, PPN dikenakan atas DPP Nilai Lain sebesar 11/12 dari subtotal, setara 11% dari subtotal, dan untuk barang yang dikenai PPnBM atas subtotal penuh. Tarif sebelumnya naik dari 10% menjadi 11% pada April 2022 berdasarkan UU 7/2021 tentang Harmonisasi Peraturan Perpajakan. Template juga mendukung transaksi non-kena-pajak dengan membuat baris PPN kondisional.
ERP Invoice PDF Generation Pipeline
─────────────────────────────────────
┌─────────────────┐
│ API Request │ GET /invoices/:id/pdf
│ (NestJS route) │
└────────┬────────┘
│
▼
┌─────────────────────────────────────┐
│ Invoice Service │
│ 1. Fetch invoice + line items │
│ 2. Fetch company branding config │
│ 3. Totals, PPN = 12% × 11/12 × DPP │
│ 4. Format currency (Rp format) │
└────────┬────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Template Engine (Handlebars) │
│ invoice.hbs → HTML string │
│ - Company logo (base64 embedded) │
│ - Line items table │
│ - Totals + PPN calculation │
│ - Payment terms, bank account │
└────────┬────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ Puppeteer / @sparticuz/chromium │
│ page.setContent(html) │
│ page.pdf({ format: 'A4', │
│ printBackground: true }) │
└────────┬────────────────────────────┘
│
▼
Buffer → res.setHeader('Content-Type', 'application/pdf')
→ res.send(pdfBuffer) (or upload to GCS)Embed logo perusahaan sebagai data URI base64 di data template Handlebars, bukan referensi path file atau URL. page.setContent() Puppeteer tidak selalu menyelesaikan path relatif atau membuat request jaringan untuk gambar eksternal.
Puppeteer di Lingkungan Cloud Run
Menjalankan Puppeteer di Cloud Run memerlukan puppeteer-core (tanpa Chromium bawaan) ditambah @sparticuz/chromium, yang menyediakan binary Chromium pre-built yang dioptimalkan untuk lingkungan serverless.
Pengaturan Halaman dan CSS Print
Saya menggunakan format A4 dengan margin atas/bawah 20mm dan kiri/kanan 15mm—standar untuk dokumen bisnis Indonesia. printBackground harus true untuk merender header tabel berwarna. Media query print CSS menangani page break untuk faktur multi-halaman.
// invoice-pdf.service.ts (NestJS)
import puppeteer from 'puppeteer-core';
import chromium from '@sparticuz/chromium';
import Handlebars from 'handlebars';
import { readFileSync } from 'fs';
import { join } from 'path';
@Injectable()
export class InvoicePdfService {
private template: HandlebarsTemplateDelegate;
constructor(private invoiceRepo: InvoiceRepository) {
const templatePath = join(__dirname, 'templates', 'invoice.hbs');
const source = readFileSync(templatePath, 'utf-8');
this.template = Handlebars.compile(source);
// Register Indonesian Rupiah formatter
Handlebars.registerHelper('rupiah', (value: number) =>
new Intl.NumberFormat('id-ID', {
style: 'currency',
currency: 'IDR',
minimumFractionDigits: 0,
}).format(value)
);
}
async generatePdf(invoiceId: string): Promise<Buffer> {
const invoice = await this.invoiceRepo.findWithLineItems(invoiceId);
const subtotal = invoice.lineItems.reduce(
(sum, item) => sum + item.qty * item.unitPrice, 0
);
// PMK 131/2024 (from 1 Jan 2025): PPN 12% on a DPP Nilai Lain of 11/12
// of the subtotal for non-luxury goods, 12% of the full subtotal for
// goods subject to PPnBM. Round to whole rupiah (0.50 and above up), as
// PER-11/PJ/2025 Art. 129 does for the Faktur Pajak, so the two match.
const taxBase = invoice.isLuxuryGoods
? subtotal
: Math.round((subtotal * 11) / 12);
const tax = Math.round((taxBase * 12) / 100);
const total = subtotal + tax;
const html = this.template({
invoice,
subtotal,
taxBase,
tax,
total,
printDate: new Date().toLocaleDateString('id-ID', {
day: 'numeric', month: 'long', year: 'numeric'
}),
});
const browser = await puppeteer.launch({
args: chromium.args,
executablePath: await chromium.executablePath(),
headless: chromium.headless,
});
try {
const page = await browser.newPage();
await page.setContent(html, { waitUntil: 'networkidle0' });
const pdf = await page.pdf({
format: 'A4',
printBackground: true,
margin: { top: '20mm', bottom: '20mm', left: '15mm', right: '15mm' },
});
return Buffer.from(pdf);
} finally {
await browser.close();
}
}
}Caching dan Penyimpanan
Saya meng-cache PDF yang dihasilkan di Cloud Storage: request pertama menghasilkan dan menyimpan PDF, dan request berikutnya langsung mengembalikan signed URL GCS. Cache dibatalkan jika faktur dimodifikasi.
Setiap launch browser Puppeteer membutuhkan 1-3 detik. Untuk volume tinggi, pertimbangkan antrian Cloud Tasks yang memproses pekerjaan PDF secara asinkron dan mengunggah hasilnya ke Cloud Storage.
Mengirim Faktur sebagai Lampiran Email
Endpoint PDF faktur digunakan kembali oleh modul otomasi email: ketika finance mengklik 'Kirim ke Pelanggan', sistem memanggil service PDF, mendapat buffer, dan melampirkannya ke email Nodemailer.
Yang Akan Saya Lakukan Berbeda
Perubahan utama yang akan saya buat adalah menggunakan job queue untuk pembuatan PDF dari awal. Ketika tabel item baris tumbuh, pembuatan PDF bisa memakan waktu 4-5 detik yang menekan timeout HTTP. Pendekatan berbasis queue memisahkan pengalaman pengguna dari waktu eksekusi Puppeteer.
Sumber & Bacaan Lanjutan
- Puppeteer — Referensi API — https://pptr.dev/api
- Handlebars.js — Panduan Memulai — https://handlebarsjs.com/guide/
- Sparticuz/chromium — Chromium untuk Serverless — https://github.com/Sparticuz/chromium
- Kemenkeu JDIH — PMK No. 131 Tahun 2024 tentang perlakuan PPN — https://jdih.kemenkeu.go.id/dok/pmk-131-tahun-2024
- DJP — PER-11/PJ/2025, Pasal 129 tentang pembulatan DPP dan PPN ke rupiah penuh — https://www.pajak.go.id/sites/default/files/lampiran/PER-11_PJ_2025_0.pdf

