We continue from Part 1 where we created controllers and views for Customer and Item models; now, let’s move onto Invoice model and get the real stuff done. If you happened to land here randomly, then first go through the following articles chronologically since these are the prerequisite before you proceed here any further:
- Invoicing app in Laravel 9 – migrations and models
- Invoicing app in Laravel 9 – invoice template using dompdf
- Invoicing app in Laravel 9 using VILT stack – Part 1
Having followed the steps given in the above articles, we’re ready to build the actual invoicing functionality; so, let’s execute following command in parent (app) folder to create InvoiceController.php:
php artisan make:controller InvoiceController --model=Invoice --resource
Then, open app/Http/Controllers/InvoiceController.php and amend it as follows;
<?php
namespace App\Http\Controllers;
use App\Models\Customer;
use App\Models\Invoice;
use App\Models\InvoiceItem;
use App\Models\Item;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Inertia\Inertia;
class InvoiceController extends Controller
{
public function index()
{
$invoices = Invoice::all();
return Inertia::render('Invoices/Index', ['invoices' => $invoices]);
}
public function create()
{
$customers = Customer::all('id','name');
$items = Item::all('id','name');
return Inertia::render('Invoices/Create', ['items' => $items, 'customers' => $customers]);
}
public function store(Request $request)
{
$validated = $request->validate([
'customer_id' => 'required',
'invoice_date' => 'required|date',
'description' => 'nullable|string',
'rows.*.item_id' => 'required',
'rows.*.quantity' => 'required|numeric',
]);
DB::transaction(function () use ($request) {
$invoice = Invoice::create([
'customer_id' => $request->input('customer_id'),
'invoice_number' => $this->createRef(),
'invoice_date' => $request->date('invoice_date'),
'amount' => 0,
'description' => $request->input('description'),
]);
$rows = $request->get('rows');
foreach($rows as $row){
InvoiceItem::create([
'invoice_id' => $invoice->id,
'item_id' => $row['item_id'],
'quantity' => $row['quantity'],
'rate' => $row['rate'],
'amount' => $row['quantity'] * $row['rate'],
]);
}
$invoice->amount = $invoice->invoiceItems->sum('amount');
$invoice->save();
});
return redirect('invoices')->with('success', 'Invoice created.');
}
public function show(Item $item)
{
}
public function edit(Item $item)
{
}
public function update(Request $request, Item $item)
{
}
public function destroy(Invoice $invoice)
{
DB::transaction(function () use ($invoice) {
foreach ($invoice->invoiceItems as $item) {
$item->delete();
}
$invoice->delete();
});
return back()->with('success', 'Invoice deleted.');
}
public function rate(Request $request)
{
$rate = Item::find($request->get('id'));
return response()->json($rate->sale_rate);
}
private function createRef()
{
$offset = 5 * 60 * 60;
$timeNdate = gmdate("d-m-Y:H:i", (time() + $offset));
$inv = "";
$last = Invoice::latest()->first();
$expNum = [];
if ($last)
{
$expNum = explode('/', $last->invoice_number);
}
$dateInfo = date_parse_from_format('d-m-Y:H:i', $timeNdate);
if (!$last) {
$inv = 'CO/INV/' . $dateInfo['year'] . '/' . $dateInfo['month'] . '/1';
} else {
$inv = 'CO/INV/' . $dateInfo['year'] . '/' . $dateInfo['month'] . '/' . ($expNum[4] + 1);
}
return $inv;
}
}
Let’s stop and analyze the above code a bit more. In the store() function, we’ve utilized DB::transaction() which makes sure that all the code within the DB::transaction() function gets executed in its entirety. This functionality is crucial when developing a financial app because on a number of occasions we have to make sure that all the entries related to a transaction are completely inserted / updated / deleted before moving ahead; if any of those entries are not entered due to some error, then we have to roll back the transaction in its entirety. Therefore, by employing DB::transaction() in the above code we have made sure that all the invoice items are properly inserted into the database along with invoice entry.
We’ve also created a separate private function createRef() for the purpose of generating invoice reference for each new invoice based on current year, month and the last invoice’s reference number. There is another extra function i.e., rate() which we’ll discuss later on.
Next, in order to define routes, open routes/web.php and put the following line at the upper side of it:
use App\Http\Controllers\InvoiceController;
use App\Models\Invoice;
use Barryvdh\DomPDF\Facade\Pdf;
And then, insert following routes down somewhere in this routes/web.php;
Route::resource('invoices', InvoiceController::class)->middleware(['auth']);
Route::get('/rate', [InvoiceController::class, 'rate'])->middleware(['auth']);
Route::get('/pdf/{id}', function ($id) {
$invoice = Invoice::find($id);
$pdf = Pdf::loadView('invoice', compact('invoice'));
return $pdf->stream($invoice->invoice_number.'.pdf');
})->name('pdf');
We’re done with the controller and routing. Now let’s create views using Vue.
First, create folder Invoices inside resources/js/Pages folder. Being inside parent folder, you may execute following command in order to make this folder:
mkdir resources/js/Pages/Invoices
Now create following two files inside resources/js/Pages/Invoices folder:
- Index.vue
- Create.vue
You can execute following command inside parent folder to create these files in one go:
touch resources/js/Pages/Invoices/{Index,Create}.vue
Then, go inside resources/js/Pages/Invoices folder and insert following code into resources/js/Pages/Invoices/Index.vue;
<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'
import PrimaryButton from '@/Components/PrimaryButton.vue'
import SecondaryButton from '@/Components/SecondaryButton.vue'
import DangerButton from '@/Components/DangerButton.vue'
import { router, Head } from '@inertiajs/vue3'
const props = defineProps({
invoices: Object,
})
const create = () => {
router.visit(route('invoices.create'))
}
const destroy = (id) => {
router.visit(route('invoices.destroy', id), {method: 'delete'})
}
</script>
<template>
<AuthenticatedLayout>
<Head title="Invoices" />
<h1 class="pl-6 py-2 text-3xl font-bold bg-gray-500 text-white">Invoices</h1>
<div v-if="$page.props.flash.success" class="bg-gray-300 font-bold text-blue-900 p-1">
{{ $page.props.flash.success }}
</div>
<div class="m-4">
<PrimaryButton @click="create()">Create invoice</PrimaryButton>
</div>
<div class="bg-white rounded-md shadow overflow-x-auto">
<table class="w-full whitespace-nowrap">
<thead>
<tr class="bg-gray-500 text-white font-extrabold">
<th class="px-4 py-1 border">Customer</th>
<th class="px-4 py-1 border">Number</th>
<th class="px-4 py-1 border">Amount</th>
<th class="px-4 py-1 border">Description</th>
<th class="px-4 py-1 border">Actions</th>
</tr>
</thead>
<tbody>
<tr v-for="invoice in invoices" :key="invoice.id">
<td class="px-4 py-1 border">{{invoice.customer_id}}</td>
<td class="px-4 py-1 border">{{invoice.invoice_number}}</td>
<td class="px-4 py-1 border">{{invoice.amount}}</td>
<td class="px-4 py-1 border">{{invoice.description}}</td>
<td class="px-4 py-1 border">
<a class="px-4 py-2 mr-4 rounded-md text-white bg-gray-500 hover:bg-gray-600" :href="'pdf/' + invoice.id" target="_blank">Generate PDF</a>
<DangerButton @click="destroy(invoice.id)">
Delete
</DangerButton>
</td>
</tr>
</tbody>
</table>
</div>
</AuthenticatedLayout>
</template>
Then insert following code into resources/js/Pages/Invoices/Create.vue:
<script setup>
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout.vue'
import InputLabel from '@/Components/InputLabel.vue'
import TextInput from '@/Components/TextInput.vue'
import PrimaryButton from '@/Components/PrimaryButton.vue'
import SecondaryButton from '@/Components/SecondaryButton.vue'
import DangerButton from '@/Components/DangerButton.vue'
import { Head, useForm, router } from '@inertiajs/vue3'
const props = defineProps({
items: Object,
customers: Object,
})
const form = useForm({
customer_id: '',
invoice_date: null,
description: null,
rows: [{
item_id:'', quantity:'', rate:'', amount:''
}],
})
const addRow = () => {
form.rows.push({item_id:'', quantity:'', rate:'', amount:''})
}
const deleteRow = (index) => {
form.rows.splice(index,1);
}
const getRate = (item,index) => {
axios.get('/rate', {params: {id: item}
})
.then(res => {
form.rows[index].rate = res.data
updateFields(index)
})
}
function updateFields(i) {
form.rows[i].amount = form.rows[i].quantity * form.rows[i].rate
}
</script>
<template>
<AuthenticatedLayout>
<Head title="Invoices" />
<h1 class="pl-6 py-2 text-3xl font-bold bg-gray-500 text-white">New Invoice</h1>
<div>
<template v-for="error in form.errors" :key="error.id">
<div class="text-red-600">{{error}}</div>
</template>
</div>
<form @submit.prevent="form.post(route('invoices.store'))">
<div class="bg-white rounded-lg shadow m-4 p-4">
<div>
<InputLabel value="Customer" class="mb-1"/>
<select v-model="form.customer_id" class="rounded-md border mb-2 border-gray-300">
<option value="">Select customer:</option>
<option v-for="customer in props.customers" :key="customer.id" :value="customer.id">
{{ customer.name }}
</option>
</select>
</div>
<div>
<InputLabel value="Invoice Date" class="mb-1"/>
<TextInput v-model="form.invoice_date" type="text" placeholder="YYYY-MM-DD" class="mb-2"/>
</div>
<div>
<InputLabel value="Description" class="mb-1"/>
<TextInput v-model="form.description" type="text" class="w-1/2 mb-2"/>
</div>
<div class="grid grid-cols-10">
<div class="col-span-2 mb-1">
<InputLabel value="Item" />
</div>
<div class="col-span-2">
<InputLabel value="Quantity" />
</div>
<div class="col-span-2">
<InputLabel value="Rate" />
</div>
<div class="col-span-2">
<InputLabel value="Amount" />
</div>
</div>
<div class="grid grid-cols-10">
<template v-for='(row, index) in form.rows' :key="index">
<div class="col-span-2 mb-1">
<select v-model="row.item_id" @change="getRate(row.item_id, index)" class="rounded-md border border-gray-300">
<option value="">Please select item:</option>
<option v-for="item in props.items" :key="item.id" :value="item.id">
{{ item.name }}
</option>
</select>
</div>
<div class="col-span-2">
<TextInput v-model="row.quantity" @change="updateFields(index)" type="text" />
</div>
<div class="col-span-2">
<TextInput v-model="row.rate" type="text" readonly/>
</div>
<div class="col-span-2">
<TextInput v-model="row.amount" type="text" readonly/>
</div>
<div class="col-span-2 flex items-center">
<DangerButton v-if="index > 0" @click.prevent="deleteRow(index)">X</DangerButton>
</div>
</template>
</div>
<div class="grid grid-cols-10">
<div class="col-span-2 mt-4">
<SecondaryButton @click.prevent="addRow" >Add Row</SecondaryButton>
</div>
<div class="col-span-2 mt-4">
<PrimaryButton type="submit" class="ml-4" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
Create Invoice
</PrimaryButton>
</div>
</div>
</div>
</form>
</AuthenticatedLayout>
</template>
Let’s analyze this code a bit. Within the form object, we’ve defined an array named rows which carries the invoice items – whenever Add Row button is pressed, addRow() function is called which adds an item line within rows array. Then, we have getRate() function hooked up with item select drop-down in each item row; whenever there’s any change in item select drop-down, an Ajax call is made to InvoiceController‘s rate() function which returns the selling rate of the respective item and the rate field of the item row gets updated dynamically. We’ve also defined updateFields() function which updates the respective amount field whenever there’s any change in quantity input field or item select drop-down.
We now wrap up our invoicing app and as a final step we provide a navigation menu to our main routes that we’ve created so far; so, let’s insert navigation links inside resources/js/Layouts/AuthenticatedLayout.vue right after the comments <!– Navigation Links –> as given below:
<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
<NavLink :href="route('dashboard')" :active="route().current('dashboard')">
Dashboard
</NavLink>
<NavLink :href="route('customers.index')" :active="route().current('customers.index')">
Customers
</NavLink>
<NavLink :href="route('items.index')" :active="route().current('items.index')">
Items
</NavLink>
<NavLink :href="route('invoices.index')" :active="route().current('invoices.index')">
Invoices
</NavLink>
</div>
For smaller screens, you should also change the Hamburger menu in similar fashion which is down below in resources/js/Layouts/AuthenticatedLayout.vue right where it’s commented <!– Responsive Navigation Menu –>
Finally, compile client side assets (JS and CSS files) by executing following command;
npm run build
Run PHP dev server by executing:
php artisan serv
And then enter the following URL in your browser to access index of Invoices (don’t forget to create a user login first, if not already done):
localhost:8000/invoices
Create some invoices, generate PDFs and chill – now you have a decent boilerplate to create most of the business solutions using Laravel 9 VILT stack. But remember, there are a lot of things missing here such as datatable with capabilities to search, filter, and paginate etc.; the purpose here is to demonstrate the core of a business app as we’d normally develop using this stack (Laravel 9 VILT stack).
Next, we’ll employ the TALL (Tailwind Alpine Laravel Livewire) stack in order to achieve the similar results. So, stay tuned 😉