finller / laravel-invoices
Store invoices safely in your Laravel application
Package info
github.com/ElegantEngineeringTech/laravel-invoices
pkg:composer/finller/laravel-invoices
Requires
- php: ^8.4
- dompdf/dompdf: ^3.1
- elegantly/laravel-money: ^4.2.0
- illuminate/contracts: ^13.0
- spatie/laravel-package-tools: ^1.14
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.1.1
- orchestra/testbench: ^11.0
- pestphp/pest: ^5.0
- pestphp/pest-plugin-arch: ^5.0
- pestphp/pest-plugin-laravel: ^5.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
- stripe/stripe-php: ^21.0
Suggests
- stripe/stripe-php: Stripe SDK is required to use the invoice from stripe checkout feature.
Provides
None
Conflicts
None
Replaces
None
- dev-main
- v6.0.1
- v6.0.0
- v5.1.4
- v5.1.3
- v5.1.2
- v5.1.1
- v5.1.0
- v5.0.0
- v4.8.0
- v4.7.2
- v4.7.1
- v4.7.0
- v4.6.6
- v4.6.5
- v4.6.4
- v4.6.3
- v4.6.2
- v4.6.1
- v4.6.0
- v4.5.1
- v4.5.0
- v4.4.7
- v4.4.6
- v4.4.5
- v4.4.4
- v4.4.3
- v4.4.2
- v4.4.1
- v4.4.0
- v4.3.6
- v4.3.5
- v4.3.4
- v4.3.3
- v4.3.2
- v4.3.1
- v4.3.0
- v4.2.2
- v4.2.1
- v4.2.0
- v4.1.3
- v4.1.2
- v4.1.1
- v4.1.0
- v4.0.3
- v4.0.2
- v4.0.1
- v4.0.0
- v3.5.5
- v3.5.4
- v3.5.3
- v3.5.2
- v3.5.1
- v3.5.0
- v3.4.1
- v3.4.0
- v3.3.6
- v3.3.5
- v3.3.4
- v3.3.3
- v3.3.2
- v3.3.1
- v3.3.0
- v3.2.1
- v3.2.0
- v3.1.2
- v3.1.1
- v3.1.0
- v3.0.0
- v2.3.7
- v2.3.6
- v2.3.5
- v2.3.4
- v2.3.3
- v2.3.2
- v2.3.1
- v2.3.0
- v2.2.4
- v2.2.3
- v2.2.2
- v2.2.1
- v2.2
- v2.1.7
- v2.1.6
- v2.1.5
- v2.1.4
- v2.1.3
- v2.1.2
- v2.1.1
- v2.1.0
- v2.0.3
- v2.0.2
- v2.0.1
- v2.0.0
- v1.0.4
- v1.0.3
- v1.0.2
- v1.0.1
- v1.0.0
- v0.1.x-dev
- v0.1.1
- v0.1.0
- dev-next
- dev-dev
This package is auto-updated.
Last update: 2026-10-04 22:06:48 UTC
README
Create, store, calculate, and render invoices, quotes, proformas, and credit notes in Laravel. The package supports multiple item-level discounts and taxes, serial numbers, PDF generation, mail attachments, polymorphic relations, Stripe Checkout imports, and GOBL export.
Amounts are denormalized: items and invoices hold their calculated amounts. Supply precomputed values from your application or payment provider, or call denormalize() to calculate missing amounts. Existing values are preserved unless you request recalculation.
There are two independent ways to use the package:
PdfInvoice: build and render invoices without a database.Invoice: store invoices and their items with Eloquent, then optionally generate PDFs or export them.
Interactive Demo
Try the interactive demo to explore the package.
Table of Contents
- Interactive Demo
- Requirements
- Installation
- Upgrading from v5
- The
PdfInvoiceClass- Full Example
- Creating PDFs from Arrays
- PDF Amounts and Denormalization
- PDF Discounts and Taxes
- Parties, Addresses, and Custom Fields
- Types, States, and Dates
- Payment Instructions and QR Codes
- Rendering, Downloading, and Storing PDFs
- Rendering Blade Views
- PDF Mail Attachments
- PDF Customization and Localization
- The
InvoiceEloquent Model- Complete Example
- Stored Amounts and Denormalization
- Managing Invoice Items
- Stored Discounts and Taxes
- Stored Parties, Fields, and Payment Instructions
- Relations, Quotes, and Credits
- Types, States, and Query Scopes
- Generating Serial Numbers
- Storing Logos
- Converting Models to PDFs
- Model Mail Attachments
- Replicating, Scaling, and Allocating Amounts
- Collections and Money Totals
- Importing Stripe Checkout Line Items
- GOBL Export
- Custom Models and Value Objects
- Casting Types and States to Enums
- Testing
- Changelog
- Contributing
- Security Vulnerabilities
- Credits
- License
Requirements
- PHP 8.4+
- Laravel 13.x
dompdf/dompdf3.1+elegantly/laravel-money4.2+
The Stripe integration additionally requires stripe/stripe-php.
Installation
composer require elegantly/laravel-invoices
For Eloquent storage, publish and run the migrations:
php artisan vendor:publish --tag="invoices-migrations"
php artisan migrate
Standalone PdfInvoice usage does not require migrations.
Configuration
php artisan vendor:publish --tag="invoices-config"
Use config/invoices.php to configure your default seller, currency, rounding, serial numbers, and PDF appearance. See the configuration file for all settings and defaults.
Upgrading from v5
See the migration guide for breaking changes, database conversions, and examples for upgrading from v5 to v6.
The PdfInvoice Class
Build PDFs, Blade previews, and mail attachments from your own data, without database storage.
Full Example
use Brick\Money\Money; use Elegantly\Invoices\Collections\InvoiceDiscountCollection; use Elegantly\Invoices\Collections\InvoiceTaxCollection; use Elegantly\Invoices\Collections\PdfInvoiceItemCollection; use Elegantly\Invoices\Enums\InvoiceState; use Elegantly\Invoices\Enums\InvoiceType; use Elegantly\Invoices\InvoiceDiscount; use Elegantly\Invoices\InvoiceTax; use Elegantly\Invoices\Pdf\PdfInvoice; use Elegantly\Invoices\Pdf\PdfInvoiceItem; use Elegantly\Invoices\Support\Address; use Elegantly\Invoices\Support\Identity; use Elegantly\Invoices\Support\Party; use Elegantly\Invoices\Support\PaymentInstruction; use Elegantly\Invoices\Support\TaxId; $pdfInvoice = new PdfInvoice( type: InvoiceType::Invoice, state: InvoiceState::Pending, serial_number: 'INV-260001', created_at: now(), due_at: now()->addDays(30), fields: ['Order' => 'PO0234'], seller: new Party( company: 'Acme Studio', name: 'Jane Doe', address: new Address( street: "Place de l'Opéra", city: 'Paris', postal_code: '75009', country: 'FR', ), tax_id: new TaxId(country: 'FR', code: '123456789'), email: 'billing@example.com', phone: '+33 1 23 45 67 89', identities: [new Identity(type: 'SIREN', code: '732829320')], fields: ['Website' => 'https://example.com'], ), buyer: new Party( company: 'Doe Corporation', name: 'John Doe', address: new Address( street: '8405 Old James St', city: 'New York', postal_code: '14609', state: 'NY', country: 'US', ), shipping_address: new Address( street: ['8405 Old James St', 'Apartment 1'], city: 'New York', postal_code: '14609', state: 'NY', country: 'US', ), email: 'john.doe@example.com', ), items: new PdfInvoiceItemCollection([ new PdfInvoiceItem( label: 'Consulting', unit_price: Money::of('100.00', 'EUR'), quantity: 2, quantity_unit: 'hours', description: 'Application development', discounts: new InvoiceDiscountCollection([ new InvoiceDiscount( code: 'WELCOME', label: 'Welcome offer', percentage: 10, ), ]), taxes: new InvoiceTaxCollection([ new InvoiceTax( type: 'vat', country: 'FR', percentage: 20, label: 'VAT France (20%)', ), ]), ), ]), description: 'Thank you for your business.', paymentInstructions: [ new PaymentInstruction( name: 'Bank transfer', description: 'Use the invoice number as the payment reference.', fields: [ 'IBAN' => 'GB12ACME12345678123456', 'SWIFT/BIC' => 'ACMEGB2L', 'Reference' => 'INV-260001', ], ), ], // Optional: logo: public_path('images/logo.png'), template: 'default.layout', templateData: ['font' => 'DejaVu Sans'], ); $pdfInvoice->denormalize(); // Subtotal: EUR 200.00; discount: EUR 20.00; // tax: EUR 36.00; total: EUR 216.00. return $pdfInvoice->stream();
Creating PDFs from Arrays
Use PdfInvoice::make() to build an invoice from nested arrays. Numeric amounts are in major units, using currency or invoices.default_currency (USD by default).
use Elegantly\Invoices\Pdf\PdfInvoice; $pdfInvoice = PdfInvoice::make([ 'type' => 'invoice', 'state' => 'pending', 'serial_number' => 'INV-260002', 'currency' => 'EUR', 'created_at' => now(), 'seller' => config('invoices.default_seller'), 'buyer' => [ 'company' => 'Doe Corporation', 'email' => 'john.doe@example.com', ], 'items' => [ [ 'label' => 'Consulting', 'unit_price' => 100.00, 'quantity' => 2, 'discounts' => [['code' => 'WELCOME', 'percentage' => 10]], 'taxes' => [['type' => 'vat', 'country' => 'FR', 'percentage' => 20]], ], ], ])->denormalize();
PDF Amounts and Denormalization
Each item holds its amounts, and the invoice holds their totals:
| Item property | Meaning | Invoice total |
|---|---|---|
unit_price |
Price of one unit before discounts and taxes. | — |
price_subtotal |
Line subtotal, normally unit_price × quantity. |
subtotal_amount |
price_discount |
Sum of the line's discount amounts. | discount_amount |
price_tax |
Sum of the line's tax amounts. | tax_amount |
price |
Final line amount: price_subtotal − price_discount + price_tax. |
total_amount |
Letting the Package Calculate Amounts
Call denormalize() to calculate line amounts and invoice totals before rendering:
$pdfInvoice->denormalize();
- Only missing (
null) amounts are calculated; existing amounts, including zero, are preserved. - If you supply only
price_subtotal, the unit price is derived from the quantity. - Calculations use
invoices.rounding_mode, defaulting toRoundingMode::HalfUp.
Rendering does not calculate amounts automatically.
Using Precomputed Amounts
Supply line amounts and invoice totals directly when they have already been calculated:
use Brick\Money\Money; use Elegantly\Invoices\Collections\PdfInvoiceItemCollection; use Elegantly\Invoices\Pdf\PdfInvoice; use Elegantly\Invoices\Pdf\PdfInvoiceItem; $pdfInvoice = new PdfInvoice( serial_number: 'INV-260003', items: new PdfInvoiceItemCollection([ new PdfInvoiceItem( label: 'Consulting', quantity: 2, unit_price: Money::of('100.00', 'EUR'), price_subtotal: Money::of('200.00', 'EUR'), price_discount: Money::of('20.00', 'EUR'), price_tax: Money::of('36.00', 'EUR'), price: Money::of('216.00', 'EUR'), ), ]), subtotal_amount: Money::of('200.00', 'EUR'), discount_amount: Money::of('20.00', 'EUR'), tax_amount: Money::of('36.00', 'EUR'), total_amount: Money::of('216.00', 'EUR'), );
You can mix precomputed and calculated amounts: denormalize() fills the gaps. To display discount and tax breakdowns, also supply the corresponding entries with their amounts.
Recalculating after Changes
After changing a quantity, unit price, discount, or tax percentage, request recalculation explicitly:
$pdfInvoice->items->first()->quantity = 3; $pdfInvoice->denormalize(force: true);
force: true recalculates line amounts and invoice totals. Percentage discounts and taxes are recalculated; fixed amounts remain inputs.
PDF Discounts and Taxes
Add percentage or fixed-amount discounts and taxes to each item:
use Brick\Money\Money; use Elegantly\Invoices\Collections\InvoiceDiscountCollection; use Elegantly\Invoices\Collections\InvoiceTaxCollection; use Elegantly\Invoices\InvoiceDiscount; use Elegantly\Invoices\InvoiceTax; use Elegantly\Invoices\Pdf\PdfInvoiceItem; $item = new PdfInvoiceItem( label: 'Consulting', unit_price: Money::of('100.00', 'EUR'), quantity: 2, discounts: new InvoiceDiscountCollection([ new InvoiceDiscount(code: 'SUMMER', label: 'Summer offer', percentage: 10), new InvoiceDiscount(label: 'Loyalty credit', amount: Money::of('5.00', 'EUR')), ]), taxes: new InvoiceTaxCollection([ new InvoiceTax(type: 'vat', country: 'FR', percentage: 20), new InvoiceTax(label: 'Fixed levy', amount: Money::of('2.00', 'EUR')), ]), ); $item->denormalize(); // Subtotal: 200.00; discounts: 25.00; taxes: 37.00; final amount: 212.00.
- Fixed amounts apply to the whole line, not each unit.
- Percentage discounts are applied sequentially, in the order provided.
- Each percentage tax is calculated on the subtotal after all discounts; taxes are not compounded.
- For an invoice-wide promotion, distribute its discount across the items.
The default template groups discount and tax breakdowns automatically. Use getDiscounts() and getTaxes() for these summaries in custom templates. Taxes can include country, state, and taxability for jurisdiction and exemption information.
Use Money::of('19.80', 'EUR') for major units or Money::ofMinor(1980, 'EUR') for minor units. If you use direct discount/tax array constructors instead of Money, their numeric amounts are in minor units; PdfInvoice::make() uses major units throughout.
Parties, Addresses, and Custom Fields
Use Party for the seller and buyer. Add billing and shipping addresses, contact details, tax IDs, registration identities, and custom fields as shown in the full example.
Custom fields can appear in the invoice header, party details, or addresses:
$pdfInvoice->fields = [ 'Order' => 'PO0234', 'Customer reference' => 'ACME-42', ];
Types, States, and Dates
Built-in document types and states have translated labels:
InvoiceType:Invoice,Quote,Credit,Proforma.InvoiceState:Draft,Pending,Paid,Refunded.
For custom labels, use your own enum implementing Elegantly\Invoices\Contracts\HasLabel.
Set created_at, due_at, and paid_at to display dates. Change their format with invoices.date_format.
Payment Instructions and QR Codes
Add bank details, payment links, and QR codes with paymentInstructions:
use Elegantly\Invoices\Support\PaymentInstruction; $pdfInvoice->paymentInstructions = [ new PaymentInstruction( name: 'Bank transfer', description: 'Pay using the bank details below.', qrcode: 'data:image/png;base64,'.base64_encode( file_get_contents(public_path('images/payment-qr.png')) ), fields: [ 'Bank' => 'Acme Bank', 'IBAN' => 'GB12ACME12345678123456', 'SWIFT/BIC' => 'ACMEGB2L', '<a href="https://example.com/pay">Pay online</a>', ], ), ];
Payment fields and invoice descriptions support HTML. Generate QR codes with a package such as chillerlan/php-qrcode.
Rendering, Downloading, and Storing PDFs
namespace App\Http\Controllers; use Elegantly\Invoices\Pdf\PdfInvoice; use Illuminate\Support\Facades\Storage; class PdfInvoiceController extends Controller { public function show() { return $this->makeInvoice()->stream(); } public function download() { return $this->makeInvoice()->download(filename: 'invoice.pdf'); } public function store() { $pdfInvoice = $this->makeInvoice(); Storage::disk('local')->put( 'invoices/'.$pdfInvoice->getFilename(), $pdfInvoice->getPdfOutput(), ); return response()->noContent(); } private function makeInvoice(): PdfInvoice { return PdfInvoice::make([ 'serial_number' => 'INV-260001', 'currency' => 'EUR', 'created_at' => now(), 'seller' => config('invoices.default_seller'), 'buyer' => ['company' => 'Doe Corporation'], 'items' => [ ['label' => 'Consulting', 'unit_price' => 100, 'quantity' => 2], ], ])->denormalize(); } }
stream() displays the PDF inline; download() downloads it. The default filename is the serial number followed by .pdf.
Livewire Downloads
namespace App\Livewire; use Elegantly\Invoices\Pdf\PdfInvoice; use Livewire\Component; class InvoicePreview extends Component { public function download() { $pdfInvoice = PdfInvoice::make([ 'serial_number' => 'INV-260001', 'currency' => 'EUR', 'seller' => config('invoices.default_seller'), 'buyer' => ['company' => 'Doe Corporation'], 'items' => [ ['label' => 'Consulting', 'unit_price' => 100, 'quantity' => 2], ], ])->denormalize(); return response()->streamDownload(function () use ($pdfInvoice) { echo $pdfInvoice->getPdfOutput(); }, $pdfInvoice->getFilename(), ['Content-Type' => 'application/pdf']); } public function render() { return view('livewire.invoice-preview'); } }
In resources/views/livewire/invoice-preview.blade.php:
<div> <button wire:click="download" wire:loading.attr="disabled"> Download invoice </button> </div>
Accessing Dompdf
pdf() returns an unrendered Dompdf instance. Override configured options, paper settings, or view data for a particular render:
$dompdf = $pdfInvoice->pdf( options: ['defaultFont' => 'DejaVu Sans'], paper: ['size' => 'a4', 'orientation' => 'landscape'], data: ['footerText' => 'Thank you for your business'], ); $dompdf->render(); $output = $dompdf->output();
Rendering Blade Views
namespace App\Http\Controllers; use Elegantly\Invoices\Pdf\PdfInvoice; class InvoicePreviewController extends Controller { public function show() { $pdfInvoice = PdfInvoice::make([ 'serial_number' => 'INV-260001', 'currency' => 'EUR', 'seller' => config('invoices.default_seller'), 'buyer' => ['company' => 'Doe Corporation'], 'items' => [ ['label' => 'Consulting', 'unit_price' => 100, 'quantity' => 2], ], ])->denormalize(); return $pdfInvoice->view(); } }
To embed the invoice in another Blade view:
<div class="aspect-[210/297] bg-white shadow-md"> @include('invoices::default.invoice', ['invoice' => $pdfInvoice]) </div>
The partial uses Tailwind CSS classes. Include the package's invoices::default.style partial or provide equivalent styles in your application. The complete default layout already includes the package's CSS.
PDF Mail Attachments
Attaching PDFs to Mailables
namespace App\Mail; use Elegantly\Invoices\Pdf\PdfInvoice; use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailables\Content; class PdfInvoiceMail extends Mailable { public function __construct(public PdfInvoice $pdfInvoice) {} public function content(): Content { return new Content(htmlString: '<p>Your invoice is attached.</p>'); } public function attachments(): array { return [ $this->pdfInvoice->toMailAttachment(filename: 'invoice.pdf'), ]; } }
Send the PDF built in the full example:
use App\Mail\PdfInvoiceMail; use Illuminate\Support\Facades\Mail; Mail::to('john.doe@example.com')->send(new PdfInvoiceMail($pdfInvoice));
Attaching PDFs to Notifications
namespace App\Notifications; use Elegantly\Invoices\Pdf\PdfInvoice; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; class PdfInvoiceNotification extends Notification { public function __construct(public PdfInvoice $pdfInvoice) {} public function via(object $notifiable): array { return ['mail']; } public function toMail(object $notifiable): MailMessage { return (new MailMessage) ->subject('Your invoice') ->line('Your invoice is attached.') ->attach($this->pdfInvoice); } }
use App\Notifications\PdfInvoiceNotification; use Illuminate\Support\Facades\Notification; Notification::route('mail', 'john.doe@example.com') ->notify(new PdfInvoiceNotification($pdfInvoice));
PDF Customization and Localization
Logos, Templates, and Template Data
Set logo to an image path or data URI, or configure a default with invoices.pdf.logo.
Publish views to customize the layout, invoice content, header, footer, and payment instructions:
php artisan vendor:publish --tag="invoices-views"
Edit files in resources/views/vendor/invoices/. For a layout at resources/views/vendor/invoices/my-custom/layout.blade.php, use template: 'my-custom.layout' or set invoices.pdf.template.
Pass custom template settings through templateData, and extra Blade variables through view($data) or pdf(data: ...). Add styles for custom classes in style.blade.php.
Fonts and PDF Options
The default layout reads font and fonts from templateData:
$pdfInvoice->templateData = [ 'font' => 'Arimo', 'fonts' => [ 'https://fonts.googleapis.com/css2?family=Arimo:ital,wght@0,400..700;1,400..700&display=swap', ], ];
You can set these defaults in invoices.pdf.template_data. Configure Dompdf's font directories, cache, remote resources, and other options through invoices.pdf.options. See the Dompdf font guide.
Localization and Money Formatting
Built-in type/state labels and template text use Laravel translations. Set your application locale and publish translations to customize them:
php artisan vendor:publish --tag="invoices-translations"
Use format_money() in custom templates to format amounts with the application locale, or specify a locale:
use Brick\Money\CurrencyDisplay; use Brick\Money\Money; use function Elegantly\Invoices\format_money; $formatted = format_money( Money::of('100.00', 'EUR'), locale: 'fr', currencyDisplay: CurrencyDisplay::Symbol, hideFractionIfWhole: true, );
Extending PdfInvoice
Extend PdfInvoice to customize behavior such as filenames:
namespace App\ValueObjects; class PdfInvoice extends \Elegantly\Invoices\Pdf\PdfInvoice { public function getFilename(): string { return 'acme-'.parent::getFilename(); } }
The Invoice Eloquent Model
Store invoice snapshots and totals with Invoice, and their line amounts, discounts, and taxes with InvoiceItem.
Publish and run the migrations before using the models. Assign amounts using Money; the package handles database storage and currency.
Complete Example
use Brick\Money\Money; use Elegantly\Invoices\Collections\InvoiceDiscountCollection; use Elegantly\Invoices\Collections\InvoiceTaxCollection; use Elegantly\Invoices\Enums\InvoiceState; use Elegantly\Invoices\Enums\InvoiceType; use Elegantly\Invoices\InvoiceDiscount; use Elegantly\Invoices\InvoiceTax; use Elegantly\Invoices\Models\Invoice; use Elegantly\Invoices\Models\InvoiceItem; use Elegantly\Invoices\Support\PaymentInstruction; use Illuminate\Support\Facades\DB; $invoice = new Invoice([ 'type' => InvoiceType::Invoice->value, 'state' => InvoiceState::Pending->value, 'state_set_at' => now(), 'seller_information' => config('invoices.default_seller'), 'buyer_information' => [ 'company' => 'Doe Corporation', 'name' => 'John Doe', 'address' => [ 'street' => ['8405 Old James St', 'Apartment 1'], 'city' => 'New York', 'postal_code' => '14609', 'state' => 'NY', 'country' => 'US', ], 'email' => 'john.doe@example.com', ], 'description' => 'Application development', 'due_at' => now()->addDays(30), 'fields' => ['Order' => 'PO0234'], 'metadata' => ['source' => 'customer-portal'], 'payment_instructions' => [ new PaymentInstruction( name: 'Bank transfer', fields: ['IBAN' => 'GB12ACME12345678123456'], ), ], ]); $invoice->configureSerialNumber( format: 'PPP-YYCCCC', prefix: 'INV', year: now()->year, ); $invoice->setItems([ new InvoiceItem([ 'label' => 'Consulting', 'unit_price' => Money::of('100.00', 'EUR'), 'quantity' => 2, 'quantity_unit' => 'hours', 'description' => 'Application development', 'discounts' => new InvoiceDiscountCollection([ new InvoiceDiscount(code: 'WELCOME', percentage: 10), ]), 'taxes' => new InvoiceTaxCollection([ new InvoiceTax(type: 'vat', country: 'FR', percentage: 20), ]), 'metadata' => ['product_id' => 'consulting'], ]), ]); DB::transaction(fn () => $invoice->denormalize()->saveWithItems()); // The invoice and its item are stored, with total_amount = EUR 216.00.
The transaction saves the invoice and its items together.
Stored Amounts and Denormalization
Line amounts and invoice totals are stored separately:
| Item attribute | Meaning | Invoice attribute |
|---|---|---|
unit_price |
Price of one unit before discounts and taxes. | — |
price_subtotal |
Normally unit_price × quantity. |
subtotal_amount |
price_discount |
Sum of line discounts. | discount_amount |
price_tax |
Sum of line taxes. | tax_amount |
price |
price_subtotal − price_discount + price_tax. |
total_amount |
Use the same currency for an invoice and its items. Numeric amounts assigned to the models are in major units.
Calculating and Saving Amounts
$invoice->denormalize()->saveWithItems();
denormalize() calculates missing line amounts and sums them into invoice totals.
- Items fill missing line amounts when saved. Call
denormalize()on the invoice to update its totals. - Existing amounts, including zero, are preserved.
- If you supply only
price_subtotal, the unit price is derived from the quantity. - Calculations use
invoices.rounding_mode, defaulting toRoundingMode::HalfUp.
Storing Precomputed Amounts
Supply amounts directly to preserve your own calculations or those of a payment provider:
use Brick\Money\Money; use Elegantly\Invoices\Collections\InvoiceDiscountCollection; use Elegantly\Invoices\Collections\InvoiceTaxCollection; use Elegantly\Invoices\InvoiceDiscount; use Elegantly\Invoices\InvoiceTax; use Elegantly\Invoices\Models\Invoice; use Elegantly\Invoices\Models\InvoiceItem; $invoice = new Invoice([ 'subtotal_amount' => Money::of('200.00', 'EUR'), 'discount_amount' => Money::of('20.00', 'EUR'), 'tax_amount' => Money::of('36.00', 'EUR'), 'total_amount' => Money::of('216.00', 'EUR'), ]); $invoice->setItems([ new InvoiceItem([ 'label' => 'Consulting', 'quantity' => 2, 'unit_price' => Money::of('100.00', 'EUR'), 'price_subtotal' => Money::of('200.00', 'EUR'), 'price_discount' => Money::of('20.00', 'EUR'), 'price_tax' => Money::of('36.00', 'EUR'), 'price' => Money::of('216.00', 'EUR'), 'discounts' => new InvoiceDiscountCollection([ new InvoiceDiscount(code: 'WELCOME', amount: Money::of('20.00', 'EUR')), ]), 'taxes' => new InvoiceTaxCollection([ new InvoiceTax(type: 'vat', country: 'FR', amount: Money::of('36.00', 'EUR')), ]), ]), ]); $invoice->saveWithItems();
You can mix precomputed and calculated amounts: denormalize() fills the gaps. Include discount and tax entries with their amounts for PDF breakdowns and exports.
Recalculating Existing Invoices
Existing derived amounts remain populated after you edit source data. Recalculate them explicitly:
$invoice->items->first()->quantity = 3; $invoice->denormalize(force: true)->saveWithItems();
force: true recalculates line amounts, percentage discounts and taxes, and invoice totals. Fixed amounts remain inputs.
Denormalizing with Artisan
Use the command to fill missing amounts or recalculate existing invoices in bulk:
# Fill missing amounts on all invoices and their items. php artisan invoices:denormalize # Limit processing to specific invoice IDs. php artisan invoices:denormalize 1 2 3 # Recalculate derived amounts, replacing existing calculated values. php artisan invoices:denormalize 1 2 3 --force
Managing Invoice Items
use Brick\Money\Money; use Elegantly\Invoices\Models\InvoiceItem; // Set the items. $invoice->setItems([ new InvoiceItem(['label' => 'Support', 'unit_price' => Money::of('50.00', 'EUR')]), ]); // Add more items. $invoice->addItems([ new InvoiceItem(['label' => 'Hosting', 'unit_price' => Money::of('10.00', 'EUR')]), ]); $invoice->denormalize(force: true)->saveWithItems();
Use saveWithItems() to persist the invoice and its items, or saveItems() for an already saved invoice. setItems() replaces the loaded collection; delete unwanted database rows explicitly through items().
You can also manage items through the items() Eloquent relation. Reload items and use denormalize(force: true) to refresh calculated totals after changes.
For fractional quantities, change the published integer quantity column to a decimal column in your application's migrations.
Deleting an invoice deletes its items by default. Set invoices.cascade_invoice_delete_to_invoice_items to false to disable the model's cascading delete behavior.
Stored Discounts and Taxes
Add percentage or fixed-amount discounts and taxes with the item helpers:
use Brick\Money\Money; use Elegantly\Invoices\InvoiceDiscount; use Elegantly\Invoices\InvoiceTax; $item = $invoice->items->first(); $item->addDiscounts(new InvoiceDiscount( code: 'SUMMER', label: 'Summer offer', percentage: 10, )); $item->addTaxes([ new InvoiceTax(type: 'vat', country: 'FR', percentage: 20), new InvoiceTax(label: 'Fixed levy', amount: Money::of('2.00', 'EUR')), ]); $invoice->denormalize(force: true)->saveWithItems();
- Discounts are applied sequentially, in the order provided.
- Each percentage tax uses the subtotal after all discounts.
- Fixed amounts apply to the whole line, not each unit.
- Taxes can include
country,state, andtaxabilityfor jurisdiction and exemption information.
Use Money for amounts. Direct discount/tax array constructors interpret numeric amounts with a currency key in minor units.
Stored Parties, Fields, and Payment Instructions
Store seller and buyer details in seller_information and buyer_information so invoices retain their original details when customer records change. Include addresses, contact details, tax IDs, and registration identities as needed.
use Elegantly\Invoices\Support\Party; use Elegantly\Invoices\Support\PaymentInstruction; $invoice->seller_information = Party::make(config('invoices.default_seller')); $invoice->buyer_information = [ 'company' => 'Doe Corporation', 'tax_id' => ['country' => 'FR', 'code' => '123456789'], 'identities' => [['type' => 'SIREN', 'code' => '732829320']], 'fields' => ['Customer number' => 'ACME-42'], ]; $invoice->mergeFields(['Order' => 'PO0234']); $invoice->metadata = ['external_id' => 'order_123']; $invoice->payment_instructions = [ new PaymentInstruction( name: 'Bank transfer', description: 'Use the invoice number as the reference.', fields: ['IBAN' => 'GB12ACME12345678123456'], ), ]; $invoice->save();
fields appear in the PDF header; mergeFields() adds to existing fields. Use metadata for application data that should not appear on the invoice.
Payment instructions appear in the PDF and can include bank details, HTML payment links, and QR codes through qrcode.
Relations, Quotes, and Credits
Associate an invoice with any Eloquent model through its polymorphic relations:
$invoice->buyer()->associate($customer); $invoice->seller()->associate($team); $invoice->invoiceable()->associate($order); $invoice->save();
Define the inverse relation on your customer model:
namespace App\Models; use Elegantly\Invoices\Models\Invoice; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\MorphMany; class Customer extends Model { public function invoices(): MorphMany { return $this->morphMany(Invoice::class, 'buyer'); } }
Invoices can also relate to other invoices through parent_id:
parent()belongs to the parent invoice.children()returns all child documents.quote()returns a child with typequote.credits()returns children with typecredit.
use Elegantly\Invoices\Models\Invoice; $credit = new Invoice(['type' => 'credit']); $credit->parent()->associate($invoice); // Add credit items and amounts, then denormalize and save.
Types, States, and Query Scopes
New invoices default to type invoice and state draft. Available values are:
- Types:
invoice,quote,credit,proforma. - States:
draft,pending,paid,refunded.
Set the state and state_set_at when your application records a payment or refund. Use query scopes to filter documents:
use Elegantly\Invoices\Models\Invoice; $paidInvoices = Invoice::invoice()->paid()->with('items')->get(); $draftQuotes = Invoice::quote()->draft()->get(); $pendingCredits = Invoice::credit()->pending()->get(); $refundedInvoices = Invoice::refunded()->get(); $proformas = Invoice::where('type', 'proforma')->get();
Type scopes are invoice(), quote(), and credit(). State scopes are draft(), pending(), paid(), and refunded().
To display a paid date in the PDF, set paid_at on the converted PdfInvoice.
Generating Serial Numbers
Serial numbers are generated automatically when an invoice is saved without one. The default format, PPYYCCCC, produces values such as IN260001.
| Token | Meaning |
|---|---|
P |
Prefix |
S |
Series |
Y |
Year |
M |
Month |
C |
Sequential count |
Repeat a token to set its width, and add separators as needed.
Multiple Prefixes and Series
use Elegantly\Invoices\Models\Invoice; $invoice = new Invoice; $invoice->configureSerialNumber( format: 'PPP-SSSS-YYMMCCCC', prefix: 'INV', serie: 42, year: 2026, month: 10, ); $invoice->save(); // First invoice in this scope: INV-0042-26100001.
Numbering is sequential within each prefix, series, year, and month. Override getPreviousInvoice() in a custom model to change that scope.
Set default numbering rules in configuration:
// In config/invoices.php, alongside the other settings: return [ 'serial_number' => [ 'auto_generate' => true, 'format' => [ 'invoice' => 'PPPYYCCCC', 'quote' => 'PPPYYCCCC', 'credit' => 'PPPYYCCCC', 'proforma' => 'PPPYYCCCC', ], 'prefix' => [ 'invoice' => 'INV', 'quote' => 'QUO', 'credit' => 'CRE', 'proforma' => 'PRO', ], ], ];
Manual Serial Numbers and Parsing
$invoice->setSerialNumber( value: 'INV-0042-26100001', format: 'PPP-SSSS-YYMMCCCC', )->save();
Manual numbers are preserved. Set invoices.serial_number.auto_generate to false if your application always supplies them.
Use SerialNumberGenerator independently to generate or parse numbers without a model:
use Elegantly\Invoices\SerialNumberGenerator; $generator = new SerialNumberGenerator('PPP-YYCCCC'); $serial = $generator->generate(prefix: 'INV', year: 26, count: 1); $parts = $generator->parse($serial); // prefix, serie, year, month, count
Storing Logos
Without a stored logo, PDFs fall back to invoices.pdf.logo. To capture a logo in the database, store its binary contents:
// Capture the current configured logo. $invoice->setLogoFromConfig(); // Or capture a local file / uploaded file. $invoice->setLogoFromPath(public_path('images/logo.png')); $invoice->setLogoFromFile($uploadedFile); // Or assign raw image bytes directly. $invoice->logo = $rawImageBytes; $invoice->save();
Converting Models to PDFs
Use toPdfInvoice() to render the stored invoice without recalculating its amounts:
namespace App\Http\Controllers; use Elegantly\Invoices\Models\Invoice; use Illuminate\Support\Facades\Storage; class InvoiceController extends Controller { public function show(Invoice $invoice) { return $invoice->loadMissing('items')->toPdfInvoice()->stream(); } public function download(Invoice $invoice) { return $invoice->loadMissing('items')->toPdfInvoice()->download(); } public function preview(Invoice $invoice) { return $invoice->loadMissing('items')->toPdfInvoice()->view(); } public function store(Invoice $invoice) { $pdfInvoice = $invoice->loadMissing('items')->toPdfInvoice(); Storage::put( 'invoices/'.$pdfInvoice->getFilename(), $pdfInvoice->getPdfOutput(), ); return response()->noContent(); } }
In routes/web.php:
use App\Http\Controllers\InvoiceController; use Illuminate\Support\Facades\Route; Route::get('/invoices/{invoice}', [InvoiceController::class, 'show']); Route::get('/invoices/{invoice}/download', [InvoiceController::class, 'download']); Route::get('/invoices/{invoice}/preview', [InvoiceController::class, 'preview']); Route::post('/invoices/{invoice}/pdf', [InvoiceController::class, 'store']);
For a Blade preview, include invoices::default.invoice with the converted PDF as invoice and include the package's CSS.
Livewire Downloads from a Model
namespace App\Livewire; use Elegantly\Invoices\Models\Invoice; use Livewire\Component; class InvoiceDownload extends Component { public Invoice $invoice; public function mount(Invoice $invoice): void { $this->invoice = $invoice; } public function download() { $pdfInvoice = $this->invoice->loadMissing('items')->toPdfInvoice(); return response()->streamDownload(function () use ($pdfInvoice) { echo $pdfInvoice->getPdfOutput(); }, $pdfInvoice->getFilename(), ['Content-Type' => 'application/pdf']); } public function render() { return view('livewire.invoice-download'); } }
In resources/views/livewire/invoice-download.blade.php:
<div> <button wire:click="download" wire:loading.attr="disabled"> Download {{ $invoice->serial_number }} </button> </div>
Mount it in a Blade view with your stored invoice:
<livewire:invoice-download :invoice="$invoice" />
Customizing PDF Output from the Model
Extend the model and override toPdfInvoice() to adjust the returned PDF:
namespace App\Models; use Elegantly\Invoices\Pdf\PdfInvoice; class Invoice extends \Elegantly\Invoices\Models\Invoice { public function toPdfInvoice(): PdfInvoice { $pdfInvoice = parent::toPdfInvoice(); $pdfInvoice->template = 'invoices::my-custom.layout'; $pdfInvoice->templateData = ['font' => 'DejaVu Sans']; $pdfInvoice->fields['Support'] = 'billing@example.com'; return $pdfInvoice; } }
Register the model through invoices.model_invoice. You can also return your own PdfInvoice subclass, or configure global PDF settings under invoices.pdf.
Model Mail Attachments
Attach a stored invoice directly to mailables and notifications.
Mailables
namespace App\Mail; use Elegantly\Invoices\Models\Invoice; use Illuminate\Mail\Mailable; use Illuminate\Mail\Mailables\Content; class InvoiceMail extends Mailable { public function __construct(public Invoice $invoice) {} public function content(): Content { return new Content(htmlString: '<p>Your invoice is attached.</p>'); } public function attachments(): array { return [$this->invoice->toMailAttachment()]; } }
use App\Mail\InvoiceMail; use Elegantly\Invoices\Models\Invoice; use Illuminate\Support\Facades\Mail; $invoice = Invoice::with('items')->findOrFail($invoiceId); Mail::to($invoice->buyer_information->email)->send(new InvoiceMail($invoice));
Customize the filename with $invoice->toPdfInvoice()->toMailAttachment(filename: 'invoice.pdf').
Notifications
namespace App\Notifications; use Elegantly\Invoices\Models\Invoice; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; class InvoiceNotification extends Notification { public function __construct(public Invoice $invoice) {} public function via(object $notifiable): array { return ['mail']; } public function toMail(object $notifiable): MailMessage { return (new MailMessage) ->subject('Your invoice') ->line('Your invoice is attached.') ->attach($this->invoice); } }
use App\Notifications\InvoiceNotification; use Elegantly\Invoices\Models\Invoice; use Illuminate\Support\Facades\Notification; $invoice = Invoice::with('items')->findOrFail($invoiceId); Notification::route('mail', $invoice->buyer_information->email) ->notify(new InvoiceNotification($invoice));
Replicating, Scaling, and Allocating Amounts
Replicating and Scaling
Replicate an invoice and its items to create a new document. For example, create a credit with negative amounts:
use Elegantly\Invoices\Enums\InvoiceType; $credit = $invoice->replicate(); $credit->type = InvoiceType::Credit->value; $credit->configureSerialNumber(format: 'PPP-YYCCCC', prefix: 'CRE'); $credit->parent()->associate($invoice); $credit->setItems($invoice->items->replicate()); $credit->multiplyBy(-1)->saveWithItems();
The copy receives a new serial number. multiplyBy() scales invoice, item, discount, and tax amounts without changing quantities or percentages. Use a string such as '0.5' for decimal factors.
Allocating Amounts
Allocate target amounts proportionally across existing items, for example to create a partial credit or match a payment provider's totals:
use Brick\Money\AllocationMode; use Brick\Money\Money; $invoice->items->allocate( subtotal: Money::of('100.00', 'EUR'), tax: Money::of('18.00', 'EUR'), discount: Money::of('10.00', 'EUR'), total: Money::of('108.00', 'EUR'), mode: AllocationMode::FloorToFirst, ); $invoice->subtotal_amount = $invoice->items->sumMoney('price_subtotal'); $invoice->discount_amount = $invoice->items->sumMoney('price_discount'); $invoice->tax_amount = $invoice->items->sumMoney('price_tax'); $invoice->total_amount = $invoice->items->sumMoney('price'); $invoice->saveWithItems();
Allocation uses existing item and breakdown amounts as proportions, and updates unit prices, discounts, and taxes. Start with calculated amounts and nonzero quantities. Save without forced denormalization to preserve the allocated result.
Collections and Money Totals
Use sumMoney() to total amounts in the same currency, or convert collections to PDFs:
use Elegantly\Invoices\Models\Invoice; $invoices = Invoice::paid()->where('currency', 'EUR')->with('items')->get(); $total = $invoices->sumMoney('total_amount'); $pdfInvoices = $invoices->toPdfInvoices(); $subtotal = $invoice->items->sumMoney('price_subtotal'); $pdfItems = $invoice->items->toPdfItems();
Importing Stripe Checkout Line Items
Install the optional Stripe SDK:
composer require stripe/stripe-php
Import Stripe Checkout line items while preserving Stripe's precomputed amounts and breakdowns:
use Elegantly\Invoices\Integrations\StripeCheckoutIntegration; use Elegantly\Invoices\Models\Invoice; use Stripe\StripeClient; $stripe = new StripeClient(config('services.stripe.secret')); $lineItems = $stripe->checkout->sessions->allLineItems($checkoutSessionId, [ 'expand' => [ 'data.price', 'data.discounts.discount.promotion_code', 'data.taxes', ], ]); $integration = new StripeCheckoutIntegration; $invoice = new Invoice([ 'state' => 'paid', 'state_set_at' => now(), 'seller_information' => config('invoices.default_seller'), 'buyer_information' => $buyerSnapshot, ]); $invoice->setItems($integration->toInvoiceItemCollection($lineItems)); $invoice->denormalize()->saveWithItems();
Use the expansions shown above and fetch all pages for larger sessions. toInvoiceItem($lineItem) imports a single line. Your application supplies invoice parties, dates, and associations; denormalize() sums the imported amounts without overwriting them.
GOBL Export
Export an invoice using the GOBL invoice schema:
use Elegantly\Invoices\Models\Invoice; use Illuminate\Support\Facades\Storage; $invoice = Invoice::with(['items', 'parent'])->firstOrFail(); $gobl = $invoice->toGOBL(); Storage::put( 'invoices/'.$invoice->serial_number.'.json', json_encode($gobl, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR), );
The export includes document details, parties, lines, discounts, taxes, payment terms, and a reference to the parent invoice where applicable.
Pass additional values to extend or override the mapping:
$gobl = $invoice->toGOBL(['regime' => 'FR']); $line = $invoice->items->first()->toGOBL([ 'item' => ['ref' => 'SKU-001'], ]);
Items, parties, discounts, and taxes can also be exported individually with toGOBL().
Custom Models and Value Objects
Extend the package classes and register your subclasses in config/invoices.php:
// In config/invoices.php, alongside the other settings: return [ 'model_invoice' => \App\Models\Invoice::class, 'model_invoice_item' => \App\Models\InvoiceItem::class, 'discount_class' => \App\ValueObjects\InvoiceDiscount::class, 'tax_class' => \App\ValueObjects\InvoiceTax::class, 'party_class' => \App\ValueObjects\Party::class, 'identity_class' => \App\ValueObjects\Identity::class, 'address_class' => \App\ValueObjects\Address::class, 'tax_id_class' => \App\ValueObjects\TaxId::class, 'payment_instructions_class' => \App\ValueObjects\PaymentInstruction::class, ];
Use seller_class and buyer_class to configure separate party classes if needed.
Factories are available through Invoice::factory() and InvoiceItem::factory(). Invoice factories include invoice(), quote(), credit(), and proforma() states.
Casting Types and States to Enums
If you prefer enum attributes, merge enum casts with the parent's casts. Also override getType() and getState() to return the cast values for PDF conversion:
namespace App\Models; use Elegantly\Invoices\Enums\InvoiceState; use Elegantly\Invoices\Enums\InvoiceType; class Invoice extends \Elegantly\Invoices\Models\Invoice { protected function casts(): array { return [ ...parent::casts(), 'type' => InvoiceType::class, 'state' => InvoiceState::class, ]; } public function getType(): InvoiceType { return $this->type; } public function getState(): InvoiceState { return $this->state; } }
Set invoices.model_invoice to this subclass. For custom enums, implement Elegantly\Invoices\Contracts\HasLabel to provide PDF labels, and configure serial numbers for your custom document types.
Testing
composer test
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.