Now that we have our Quotes being created, we should have the ability to generate a PDF and send it to our customer:
In this lesson, we will do the following:
- Create a view page for our Quote - this will be a page to preview PDF
- Install and modify a PDF generation package
- Create a controller to generate the PDF (both preview and download)
Creating a Simple View Page for Quote
We can quickly create a new View page by running the following command:
php artisan make:filament-page ViewQuote --resource=QuoteResource --type=ViewRecord
This will generate a file, but we still need to register it in our Resource:
app/Filament/Resources/QuoteResource.php
// ... public static function table(Table $table): Table { return $table // ... ->bulkActions([ Tables\Actions\BulkActionGroup::make([ Tables\Actions\DeleteBulkAction::make(), ]), ]); ]) ->recordUrl(function ($record) { return Pages\ViewQuote::getUrl([$record]); }); } public static function getPages(): array{ return [ 'index' => Pages\ListQuotes::route('/'), 'create' => Pages\CreateQuote::route('/create'), 'view' => Pages\ViewQuote::route('/{record}'), 'edit' => Pages\EditQuote::route('/{record}/edit'), ];}
Now we can click on the row, and we should see our new page:
That's it for now. Next, we will generate the PDF and display it on the View page.
Installing PDF Package
Next, we will install a package Laravel Invoices to deal with the invoice generation itself:
Note: This can be replaced with any other package or just pure DomPDF
composer require laraveldaily/laravel-invoices:^3.1
Then, we need to publish the package files:
php artisan invoices:install
This will create quite a few files, but we will only modify a few. First, let's open a config file and change our currency format:
config/invoices.php
// ... 'currency' => [ 'code' => 'eur', 'code' => 'usd', /* * Usually cents * Used when spelling out the amount and if your currency has decimals. * * Example: Amount in words: Eight hundred fifty thousand sixty-eight EUR and fifteen ct. */ 'fraction' => 'ct.', 'symbol' => '€', 'symbol' => '$', /* * Example: 19.00 */ 'decimals' => 2, /* * Example: 1.99 */ 'decimal_point' => '.', /* * By default empty. * Example: 1,999.00 */ 'thousands_separator' => '', /* * Supported tags {VALUE}, {SYMBOL}, {CODE} * Example: 1.99 € */ 'format' => '{VALUE} {SYMBOL}', 'format' => '{SYMBOL}{VALUE}',], 'seller' => [ // ... 'attributes' => [ // ... 'custom_fields' => [ /* * Custom attributes for Seller::class * * Used to display additional info on Seller section in invoice * attribute => value */ 'SWIFT' => 'BANK101', ], ],],
Then we can open the English translation file and change the Invoice
to Quote
:
resources/lang/vendor/invoices/en/invoice.php
// ... 'invoice' => 'Invoice','invoice' => 'Quote','serial' => 'Serial No.','date' => 'Invoice date','date' => 'Quote date', // ...
Generating PDF
Now that our settings are in place and we have the package, we can work on the invoice generation. First, we need to create a new controller:
php artisan make:controller QuotePdfController
Then, we can open the Controller and add the following code:
app/Http/Controllers/QuotePdfController.php
use App\Models\Quote;use Illuminate\Http\Request;use LaravelDaily\Invoices\Classes\Buyer;use LaravelDaily\Invoices\Classes\InvoiceItem;use LaravelDaily\Invoices\Invoice; class QuotePdfController extends Controller{ public function __invoke(Request $request, Quote $quote) { $quote->load(['quoteProducts.product', 'customer']); $customer = new Buyer([ 'name' => $quote->customer->first_name . ' ' . $quote->customer->last_name, 'custom_fields' => [ 'email' => $quote->customer->email, ], ]); $items = []; foreach ($quote->quoteProducts as $product) { $items[] = (new InvoiceItem()) ->title($product->product->name) ->pricePerUnit($product->price) ->subTotalPrice($product->price * $product->quantity) ->quantity($product->quantity); } $invoice = Invoice::make() ->sequence($quote->id) ->buyer($customer) ->taxRate($quote->taxes) ->totalAmount($quote->total) ->addItems($items); if ($request->has('preview')) { return $invoice->stream(); } return $invoice->download(); }}
Here's what our code does:
- It loads relationships (to prevent N+1 issue!)
- Forms a buyer with Customer details
- Forms an array of items (products) with the
InvoiceItem
class - Creates an Invoice with all the data
- If the
preview
parameter is present, it streams the PDF - Otherwise, it downloads the PDF
Now we can register our new route:
routes/web.php
use App\Http\Controllers\QuotePdfController; // ... Route::middleware('signed') ->get('quotes/{quote}/pdf', QuotePdfController::class) ->name('quotes.pdf');
Displaying PDF in View Page
Last on our List is the PDF display on the view page and the ability to download it. To do that, we need to add an InfoList:
app/Filament/Resources/QuoteResource.php
use Filament\Infolists\Components\ViewEntry;use Filament\Infolists\Infolist; // ... public static function infolist(Infolist $infolist): Infolist{ return $infolist ->schema([ ViewEntry::make('invoice') ->columnSpanFull() ->viewData([ 'record' => $infolist->record ]) ->view('infolists.components.quote-invoice-view') ]);} // ...
We simply added a ViewEntry with a custom view. Now we can create the view:
resources/views/infolists/components/quote-invoice-view.blade.php
<iframe src="{{ URL::signedRoute('quotes.pdf', [$record->id, 'preview' => true]) }}" style="min-height: 100svh;" class="w-full"> </iframe>
Here, we created an iframe with a signed URL to our PDF controller. Now, visiting the View page, we should see a PDF:
Few Small Adjustments
There are still a couple of minor things we should do:
- Add a button to download the PDF on our View page
- Remove
Please pay until:
from our Quote as it's not needed
Let's start with the button. We can add it to our View page:
app/Filament/Resources/QuoteResource/Pages/ViewQuote.php
use Filament\Actions\Action;use URL; // ... protected function getHeaderActions(): array{ return [ Action::make('Edit Quote') ->icon('heroicon-m-pencil-square') ->url(EditQuote::getUrl([$this->record])), Action::make('Download Quote') ->icon('heroicon-s-document-check') ->url(URL::signedRoute('quotes.pdf', [$this->record->id]), true), ];}
Now, loading the page, we should see a new button:
Next, we can modify our Invoice template that was published to comment out the pay until
text:
resources/views/vendor/invoices/templates/default.blade.php
{{-- ... --}} <p> {{ trans('invoices::invoice.pay_until') }}: {{ $invoice->getPayUntilDate() }}</p> {{-- ... --}}
Now we can reload the page, and we should see the text removed:
That's it! Now, our users can create quotes for their Customers and send them out as PDFs.
i culd not install composer require laraveldaily/laravel-invoices:^3.0 but had success with composer require laraveldaily/laravel-invoices:^3.1
Hmm, I think I made a mistake here. Updating!
yea the latest version as of 11/16/2023 is 3.2.0 and their has been a few changes such as where the file resources/lang/vendor/invoices/en/invoice.php is being named and where it is getting put I found it in resources/views/vendor/invoices/templates/default.blade.php
well, actually package now follows laravel change, it was due
https://laravel.com/docs/9.x/upgrade#the-lang-directory
Ok I am using Laravel 10.32.1; I am VERRY SORRY I was looking in the wrong file I have found the lang file in the resources NOT in the views file my bad! Found it after I tuck a brake "to many hours on the computer".
yeay, finish the course
I have a substantive question, why is there an Invoice and not a Quote ? The invoice is for the items sold and not for the quote
We are using an Invoice based package to generate a Quote, but not to use it as an actual invoice.
It works pretty much the same, with the only difference on what is inside and how it's given to a customer
Ok doing grate up to this point however when installing the latest version 3.2.0 of laraveldaily/ laravel-invoices it is not creating the resources/lang/vendor/invoices/en/invoice.php file. So, where do we make the changes you are telling us to make in this file?
well, actually package now follows laravel change, it was due
https://laravel.com/docs/9.x/upgrade#the-lang-directory
I'm sorry, but I'm really confused here. I checked the article, updated the package and everything still matches. I'm really not sure what you had missmatched there but it seems like it's not an issue at all.
In my version of Laravel the “LANG/VENDOR” file is in its only file under the database and above the PUBLIC and I was able to fix it thanks. It appears as though Filament might have moved it and placed a lot of its files in it.
ps. I have this in a github repository if you want to see it.
I am really confused by what you just said here :) But sure, you can share the repo and I'll take a look.
Some elements are useful, the steps could be more detailed, it may be understood that people already have knowledge and necessary steps and explanations are omitted. But it can be done better!
Thanks for your time to create this incredible course