finller/laravel-invoices
Generate professional invoices in Laravel with a fluent, model-driven API. Build PDF or HTML invoices with line items, taxes and discounts, and customize output through drivers and Blade views. Includes serial numbers and multiple invoice formats.
Requirements. Laravel 10+ and PHP 8.2+. Recent versions render PDFs through spatie/laravel-pdf, which means you also need Node and a Chromium binary available to PHP — this is the single biggest setup hurdle. In Docker, install chromium, fonts-liberation, and fonts-noto in the image (see Gotchas).
composer require finller/laravel-invoices
php artisan vendor:publish --tag="invoices-config"
php artisan vendor:publish --tag="invoices-migrations"
php artisan vendor:publish --tag="invoices-views" # Blade templates for the PDF
php artisan migrate
If you use the Browsershot/Puppeteer driver:
npm install puppeteer
npx puppeteer browsers install chrome
Where to look first. Open config/invoices.php before writing any code. It controls the storage disk, the seller/buyer/item model classes you can swap, the serial-number format, the default Blade template, and the PDF driver. Then read the published migrations — they tell you exactly which columns exist (serial_number, status, seller, buyer, items, due_at, paid_at) and therefore what the fluent API can set. The published views in resources/views/vendor/laravel-invoices are your layout starting point.
First use case. Persist an invoice with line items and hand the buyer a PDF:
use Finller\Invoice\Models\Invoice;
use Finller\Invoice\Models\InvoiceItem;
$invoice = Invoice::make()
->seller($company) // your biller
->buyer($customer) // the billed party
->addItem(
InvoiceItem::make()
->name('Consulting — March')
->quantity(12)
->unitPrice(9500) // minor units, see config
->vatRate(20)
)
->dueAt(now()->addDays(30))
->save(); // allocates a serial number + persists
return $invoice->pdf()->download("{$invoice->serial_number}.pdf");
The chain is fluent and every setter maps to a column; confirm exact method names against the README for the version you install, since the API moved between v1 and v2.
Wrap creation in an action class, always inside a transaction. Serial numbers are allocated on save, and you do not want a half-written invoice with a consumed number if an item insert fails.
final class IssueInvoice
{
public function handle(Order $order): Invoice
{
return DB::transaction(function () use ($order) {
$invoice = Invoice::make()
->seller($order->merchant->billingProfile)
->buyer($order->customer->billingProfile)
->dueAt(now()->addDays($order->paymentTerms()));
foreach ($order->lines as $line) {
$invoice->addItem(InvoiceItem::make()
->name($line->description)
->quantity($line->qty)
->unitPrice($line->unit_price_minor)
->vatRate($line->vat_rate));
}
return tap($invoice)->save();
});
}
}
Generate and store the PDF out-of-band. Rendering is seconds, not milliseconds — don't do it in the request that creates the invoice. Store to a private disk and keep the path on the invoice (or a documents table):
final class RenderInvoicePdf implements ShouldQueue
{
public function handle(InvoicePdfRenderer $renderer): void
{
$path = "invoices/{$this->invoice->serial_number}.pdf";
Storage::disk(config('invoices.disk'))
->put($path, $renderer->html($this->invoice)->toPdf());
$this->invoice->update(['pdf_path' => $path]);
}
}
Serve it through a signed, authorized route — never a public URL, invoices leak customer data.
Snapshot the seller and buyer onto the invoice. Store the address, VAT number, and legal name as they were at issue time. If you reference the Company model live, last year's PDFs silently mutate when someone edits the billing address, which is both a support nightmare and a compliance problem.
Use one InvoiceSerie per document type and fiscal year. Quotes, invoices, credit notes, and proformas must each have their own gapless sequence. Allocate the serial at finalize time, not when the draft is created, so abandoned drafts don't burn numbers.
Drive status transitions explicitly. Model draft → pending → paid (or cancelled) in your domain, not by mutating the column ad hoc. Credit notes should be a separate invoice with a negative total referencing the original, not a deletion.
Custom branding via published views. Copy resources/views/vendor/laravel-invoices into your app, edit the Blade files, and point config('invoices.template') at yours — or store a template key per invoice if you support multiple brands. Keep layout in Blade, and move the "which template / which logo / which footer" decision into config or a per-tenant setting.
Testing. Fake PDF rendering so the suite doesn't shell out to Chromium: Pdf::fake() (from spatie/laravel-pdf) and assert on the HTML/Blade output instead. Assert on serial numbers, totals, VAT breakdown, and rounding — snapshot the rendered HTML for a golden-file test.
node_modules path is wrong in a container. Set the executable path explicitly in the config, and install fonts in the image — otherwise €, £, or Cyrillic glyphs render as tofu boxes in an otherwise "working" PDF.0.30000000000000004 lines on totals once VAT and discounts stack. Verify line-level vs document-level rounding with a 3×€0.333 style test.lockForUpdate() on the serie row inside the transaction, or rely on the package's increment plus a unique index on (serie_id, serial_number) so collisions fail loudly instead of silently duplicating.composer update will not reproduce the same document. Persist the byte stream, not just the parameters.php artisan view:clear && php artisan config:clear, otherwise you'll debug a layout that isn't the one being rendered.Invoice/InvoiceItem Eloquent models, keep the table names, and register your classes in config/invoices.php. Same for the storage disk — use Storage::disk(...), never public_path().de_DE locale with a USD invoice quietly produces the wrong separators.Invoice model source once rather than treating it as a black box, and keep your own InvoicePdfRenderer interface around it so you can swap the engine without rewriting call sites.How can I help you explore Laravel packages today?