laraveldaily/laravel-invoices
This Laravel package generates Invoice PDFs from your data and stores, downloads, or streams them on any configured filesystem. It supports customizable templates, locales, taxes, discounts, shipping, due dates, serial numbers, currency formatting, and global settings with on-the-fly overrides.
Install and publish (5 minutes)
composer require laraveldaily/laravel-invoices:^4.1.1
php artisan invoices:install
invoices:install publishes three things you will actually touch: config/invoices.php, the Blade templates under resources/views/vendor/invoices/templates/ (starting with default.blade.php), and the translation files under lang/vendor/invoices/.
Look at these first, in this order
config/invoices.php — this is where 80% of your day-to-day customization lives: seller (your company's name/address/VAT/IBAN), currency (symbol, decimals, separators, {VALUE} {SYMBOL} format), serial_number (series, padding, {SERIES}{DELIMITER}{SEQUENCE}), date.pay_until_days, paper.size, and the default disk.resources/views/vendor/invoices/templates/default.blade.php — the entire visual layout is one Blade file. Read it once; it tells you exactly which $invoice->... accessors are available and how custom_fields are printed.First use case: a receipt from a controller
$customer = new Buyer(['name' => 'John Doe', 'custom_fields' => ['email' => '[email protected]']]);
$invoice = Invoice::make()
->buyer($customer)
->discountByPercent(10)
->taxRate(15)
->shipping(1.99)
->addItem(InvoiceItem::make('Service 1')->pricePerUnit(2));
return $invoice->stream();
Set the seller once in config/invoices.php instead of calling ->seller() on every invoice — that is the single highest-leverage five minutes of setup.
Smoke test before you touch a controller: call ->toHtml() and dump it in a route or tinker. If the HTML looks right, the PDF will look right; debugging the Blade output is far faster than debugging a binary.
Treat the package as a renderer, not a data store. It has no models, no migrations, no persistence, and no invoice-number registry. The pattern that holds up in production:
invoices + invoice_items tables (or orders) are the source of truth.InvoiceFactory/InvoiceBuilder service maps an Eloquent model → the fluent chain.Serial numbers are yours to manage. The package formats a sequence you hand it; it never increments anything. Use a dedicated table with a row per series and take a lock inside a transaction:
DB::transaction(function () use ($order) {
$seq = InvoiceSequence::where('series', 'BIG')->lockForUpdate()->firstOrFail();
$seq->increment('sequence');
$invoice = Invoice::make()
->series('BIG')
->sequence($seq->sequence)
->serialNumberFormat('{SERIES}/{SEQUENCE}');
// ... persist serial + PDF reference
});
Do this at issue time, not at render time, or concurrent requests will hand out duplicate invoice numbers.
Generation flow for anything user-facing:
$invoice = $this->buildFromOrder($order)
->filename("invoice-{$order->serial_number}")
->save('invoices'); // private disk, not public
// later, from a route or a signed URL:
return Storage::disk('invoices')->download($invoice->filename . '.pdf');
For S3/private disks, ignore ->url() and generate a Storage::temporaryUrl() — see gotchas. For email, save() then attach from the disk path rather than stream(); and for large volume, do the save() inside a queued job and mail a link, because PDF rendering is CPU-bound and blocks the request.
Templates per brand/tenant. Copy default.blade.php to my_company.blade.php, edit, and call ->template('my_company'). To pass data the standard accessors don't cover, use ->setCustomData($payload) and read $invoice->getCustomData() inside your template — that's the sanctioned escape hatch. Keep a distinct filename for your custom template; don't edit default.blade.php in place if you ever want to php artisan invoices:update cleanly.
Global config vs. per-invoice overrides. Everything in config can be overridden fluently. Practical split: seller identity, default currency, default paper and serial format live in config; currency (for multi-currency tenants), date, payUntilDays, status, and template are set per invoice. Multi-tenant: don't rely on the config seller at all — instantiate a Party from the tenant record and pass it with ->seller().
Item-level vs. invoice-level math. Item discounts/taxes (discount(), discountByPercent(), taxByPercent(), units()) flow into the invoice totals automatically. Adding units to any item changes the template's column set — check the rendered output the first time you mix unitless and united items.
Testing. Don't assert on PDF bytes. Assert on ->toHtml() for content and structure, and do one feature test per route asserting Content-Type: application/pdf and a %PDF prefix. For totals, build a fixture with a known item mix and assert totalAmount, totalTaxes, totalDiscount, taxableAmount directly — those are plain accessors, so this doubles as a regression test for your rounding rules.
Facade for terse call sites. Invoice::makeParty([...]) and Invoice::makeItem('X')->pricePerUnit(9.99) are handy in seeders, tests, and admin preview endpoints where you just want a throwaway invoice.
License is GPL-3.0. This is the biggest single consideration and it isn't in the feature list. For an internal tool or an unmodified SaaS backend it is usually fine, but if you ship or distribute the application, GPL's copyleft can oblige you to release source. Many companies blanket-ban GPL dependencies. Get this cleared before you write the integration, not after.
It is a document renderer, not a billing system. No persistence, no numbering enforcement, no idempotency, no audit trail, no credit notes. Everything an accountant cares about (immutable issued invoices, gapless sequences, corrections) you build. The README's "evolving fast" framing plus a single maintainer is a fair signal to pin ^4.1.1 and read the changelog before any composer update.
Floats everywhere. Amounts are floats, and totals are computed by the package in floats. 0.1 + 0.2 problems show up as 1,00 vs 1,01 on the last line. Round deliberately: compute in integer cents in your own code, convert at the boundary, and consider passing your authoritative totalAmount(), totalTaxes(), taxableAmount(), and totalDiscount() explicitly. Note the flip side: once you set those, the automatic calculation is bypassed — so a mismatch between your overrides and the line items will render an invoice that doesn't add up. Never set them "just to fix the total"; fix the inputs.
->url() on a private disk. It returns a disk URL, which for local/private S3 is not reachable by the buyer. Use Storage::disk($disk)->temporaryUrl($path, now()->addDays(7)) or a signed route.
Output-before-headers corrupts PDFs. stream() and download() emit headers plus binary. Any stray whitespace, a dd() left behind, a BOM in a PHP file, or a middleware that echoes something will produce a broken file that opens as "damaged". Same category of problem in Livewire/Octane contexts — always return the response object directly, never echo around it. ob_clean() before calling stream() if you must.
Config is global mutable state. Per-invoice paper size isn't in the method list — it's config('invoices.paper.size'). If you mutate config at runtime before building an invoice, remember that under Octane/queues those changes leak between requests unless you restore them. Prefer a custom template with fixed @page CSS over runtime config mutation.
Dompdf's CSS and font limitations. It's not a browser: no flexbox, no grid, limited position. Templates use tables for a reason. Unicode currency symbols (₹, €, ₽) and non-Latin scripts need bundled fonts — the default font set will render tofu boxes. Also, remote images and logo() with a URL are fragile; getLogo() returns a base64-encoded image precisely to dodge path resolution issues, so use it in custom templates. The README mentions this obliquely; it's the number-one "why is my logo blank" cause.
invoices:update overwrites your work. It prompts, but a distracted "yes" destroys your customized default.blade.php and translations. Keep customizations in distinctly-named files (my_company.blade.php) and treat the vendor-published default as read-only.
Filenames. The auto filename derives from the serial number; ->filename() is applied verbatim. The README example concatenates party names, which yields spaces, slashes, and unicode — those flow into storage paths and Content-Disposition. Sanitize with Str::slug() or use the serial number.
Dates and timezones. date() takes a CarbonInterface and getDate() formats it per dateFormat; payUntilDays() is calendar days from the invoice date. If you store UTC and display in a local timezone, an invoice issued at 23:30 UTC can land on the wrong calendar day — set ->date($order->issued_at->copy()->setTimezone(config('app.timezone'))) explicitly.
Queues. Never serialise an Invoice object into a job — it holds parties, items, and generated state. Pass IDs and rebuild. Same for stream(), which is meaningless inside a job.
Debugging order: toHtml() → check data → check custom template → only then look at the PDF. If toHtml() throws, it's your object graph (missing buyer/seller, bad custom_fields shape); if the PDF is wrong but HTML is right, it's dompdf (fonts, CSS, images, paper size).
Extension points worth knowing: config('invoices.seller.class') lets you swap in your own class (extend Party or implement PartyContract) so tenant/company data is resolved centrally at build time; the template() method for layout variants; setCustomData()/getCustomData() for template-only data; and the translation namespace (invoices::invoice.*) for localized statuses like ->status(__('invoices::invoice.paid')). Those four cover almost every requirement that isn't already a fluent method.
How can I help you explore Laravel packages today?