When working with invoices, you need to deal with serial numbers that look like ABC-000001
. Do you know how to auto-generate them in Laravel? This tutorial will cover a few ways to do this.
DB Structure
For our example, we will use a simple invoice DB table with the following columns:
-
id
-
user_id
-
due_date
-
amount
-
serial
- Full serial number likeABC-1
-
serial_number
- Serial number like1
-
serial_series
- Serial series likeABC
Here's how that looks in our migration:
Migration
Schema::create('invoices', function (Blueprint $table) { $table->id(); $table->foreignIdFor(User::class)->constrained(); $table->date('due_date'); $table->integer('amount'); $table->string('serial')->nullable(); $table->string('serial_series'); $table->integer('serial_number')->nullable(); $table->timestamps();});
This makes our Model look like this:
app/Models/Invoice.php
use Illuminate\Database\Eloquent\Casts\Attribute;use Illuminate\Database\Eloquent\Factories\HasFactory;use Illuminate\Database\Eloquent\Model;use Illuminate\Database\Eloquent\Relations\BelongsTo; class Invoice extends Model{ use HasFactory; protected $fillable = [ 'user_id', 'due_date', 'amount', 'serial', 'serial_number', 'serial_series', ]; protected function amount(): Attribute { return Attribute::make( get: fn($value) => $value / 100, set: fn($value) => $value * 100, ); } public function user(): BelongsTo { return $this->belongsTo(User::class); }}
Create Invoice: Form
Let's create the Controller methods to save a new invoice and auto-generate the serial number.
This is the form, with series options coming from the config.
app/Http/Controllers/InvoiceController.php
use App\Http\Requests\StoreInvoiceRequest;use App\Models\Invoice;use App\Models\User; class InvoiceController extends Controller{ public function create() { $users = User::pluck('name', 'id'); $invoiceSeries = config('invoiceSettings.availableInvoiceSeries'); return view('invoices.create', [ 'users' => $users, 'invoiceSeries' => $invoiceSeries, ]); }}
These are the config values:
config/invoiceSettings.php
return [ 'availableInvoiceSeries' => [ 'ABC', 'DAF', 'GHI', 'UKS' ],];
And here's the form in Blade file:
resources/views/invoices/create.blade.php
// ... layout with Tailwind/Breeze <form action="{{ route('invoice.store') }}" method="POST"> @csrf <div class="mb-4"> <label for="user_id" class="block text-gray-700 text-sm font-bold mb-2">User</label> <select id="user_id" name="user_id" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"> <option value="">Select User</option> @foreach($users as $id => $name) <option value="{{ $id }}" @selected(old('user_id') == $id)>{{ $name }}</option> @endforeach </select> @error('user_id') <div class="text-red-500 mt-2 text-sm"> {{ $message }} </div> @enderror </div> <div class="mb-4"> <label for="serial_series" class="block text-gray-700 text-sm font-bold mb-2">Invoice Series</label> <select id="serial_series" name="serial_series" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"> @foreach($invoiceSeries as $seriesCode) <option value="{{ $seriesCode }}" @selected(old('serial_series') == $seriesCode)>{{ $seriesCode }}</option> @endforeach </select> @error('serial_series') <div class="text-red-500 mt-2 text-sm"> {{ $message }} </div> @enderror </div> <div class="mb-4"> <label for="due_date" class="block text-gray-700 text-sm font-bold mb-2">Due Date</label> <input type="date" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" id="due_date" name="due_date" value="{{ old('due_date') }}"> @error('due_date') <div class="text-red-500 mt-2 text-sm"> {{ $message }} </div> @enderror </div> <div class="mb-4"> <label for="amount" class="block text-gray-700 text-sm font-bold mb-2">Amount</label> <input type="text" class="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline" id="amount" name="amount" value="{{ old('amount') }}"> @error('amount') <div class="text-red-500 mt-2 text-sm"> {{ $message }} </div> @enderror </div> <div class="mb-4"> <button type="submit" class="bg-blue-500 text-white px-4 py-3 rounded font-medium w-full">Create Invoice </button> </div> </form>
Next, we'll look at how to save the new invoice and generate the serial numbers differently.
Option 1: Generate Serial Number in Controller
The first option is to generate the serial number when the invoice is created. This is the simplest option, and it looks like this:
app/Http/Controllers/InvoiceController.php
// ... public function store(StoreInvoiceRequest $request): RedirectResponse{ $data = $request->validated(); $data['serial_number'] = (Invoice::where('serial_series', $data['serial_series'])->max('serial_number') ?? 0) + 1; $data['serial'] = $data['serial_series'] . '-' . $data['serial_number']; Invoice::create($data); return redirect()->route('invoice.index');} // ...
And while this works, there are better options in my eyes.
You must remember to add this code to every place you create an invoice if there are multiple places, like an API Controller.
Code in Repository
Option 2: Generate Serial Number in Model Observers
Another option for generating an invoice number is to use Observers:
app/Models/Invoice.php
// ... protected static function booted(): void{ parent::booted(); self::creating(static function (Invoice $invoice) { $invoice->serial_number = (Invoice::where('serial_series', $invoice->serial_series)->max('serial_number') ?? 0) + 1; $invoice->serial = $invoice->serial_series . '-' . $invoice->serial_number; });} // ...
This allows us to drop a big piece of code from our Controller:
app/Http/Controllers/InvoiceController.php
// ... public function store(StoreInvoiceRequest $request): RedirectResponse{ $data = $request->validated(); $data['serial_number'] = (Invoice::where('serial_series', $data['serial_series'])->max('serial_number') ?? 0) + 1; $data['serial'] = $data['serial_series'] . '-' . $data['serial_number']; Invoice::create($data); Invoice::create($request->validated()); return redirect()->route('invoice.index');} // ...
We are making it much cleaner and providing an option to create an invoice in any way we want without worrying about the serial number.
Code in Repository
Option 2B: Moving Observer to Separate File
It's important to mention that Observers can be a separate file, too. They don't have to be defined in our Models and can be used as a dedicated Observer for a Model.
php artisan make:observer InvoiceObserver --model=Invoice
Here's how that looks:
app/Observers/InvoiceObserver.php
use App\Models\Invoice; class InvoiceObserver{ public function creating(Invoice $invoice): void { $invoice->serial_number = (Invoice::where('serial_series', $invoice->serial_series)->max('serial_number') ?? 0) + 1; $invoice->serial = $invoice->serial_series . '-' . $invoice->serial_number; }}
Of course, this has to be registered:
app/Models/Invoice.php
use App\Observers\InvoiceObserver;use Illuminate\Database\Eloquent\Attributes\ObservedBy; #[ObservedBy([InvoiceObserver::class])]class Invoice extends Model { // ...}
And that's it! Now, we have a dedicated Observer for our Invoice Model.
Code in Repository
Sadly, this observer way still has a flaw: we can accidentally make duplicate serial numbers. This is not good, as any accountant will give you a hard time.
Let's take a look at another option.
Option 3: Generate Serial Number Using Jobs
Our third option includes a solution to the duplicate serial number problem - jobs and a unique index! They run in the background and can be re-run if something goes wrong. Here's how it looks:
We need to ensure we have a unique index in our migration.
Migration
Schema::create('invoices', function (Blueprint $table) { $table->id(); $table->foreignIdFor(User::class)->constrained(); $table->date('due_date'); $table->integer('amount'); $table->string('serial')->nullable(); $table->string('serial_series'); $table->integer('serial_number')->nullable(); $table->timestamps(); $table->unique(['serial_series', 'serial_number']);});
Then, we can create our Job that will generate the serial number and re-run if the serial number is already taken:
app/Jobs/GenerateInvoiceNumber.php
// ...private Invoice $invoice; public function __construct(int $invoiceID){ $this->onQueue('invoiceNumbersQueue'); $this->invoice = Invoice::findOrFail($invoiceID);} public function handle(): void{ $this->invoice->serial_number = (Invoice::where('serial_series', $this->invoice->serial_series)->max('serial_number') ?? 0) + 1; $this->invoice->serial = $this->invoice->serial_series . '-' . $this->invoice->serial_number; $this->invoice->save();}// ...
And then we can dispatch this Job in our controller:
app/Http/Controllers/InvoiceController.php
use App\Jobs\GenerateInvoiceNumberJob; // ...public function store(StoreInvoiceRequest $request): RedirectResponse{ $invoice = Invoice::create($request->validated()); dispatch(new GenerateInvoiceNumberJob($invoice->id)); return redirect()->route('invoice.index');}// ...
As a last step, we should make sure that we are running the queue worker:
php artisan queue:work --queue=invoiceNumbersQueue
Running a specific queue is important as we want invoices to have their worker processing only invoices and not other jobs. This way, you would expect your jobs to run one after another rather than in parallel, which could cause duplicates.
Code in Repository
Option 4: Why Not BOTH? Using Observers and Jobs
As our last option, we can combine two examples here - Observers and Jobs. This will solve our problem of invoice creation from anywhere and still use a dedicated queue for invoice numbers that we can re-run if something goes wrong. Here's how it looks:
The first thing we need to do is to create a Job:
app/Jobs/GenerateInvoiceNumber.php
// ...private Invoice $invoice; public function __construct(int $invoiceID){ $this->onQueue('invoiceNumbersQueue'); $this->invoice = Invoice::findOrFail($invoiceID);} public function handle(): void{ $this->invoice->serial_number = (Invoice::where('serial_series', $this->invoice->serial_series)->max('serial_number') ?? 0) + 1; $this->invoice->serial = $this->invoice->serial_series . '-' . $this->invoice->serial_number; $this->invoice->save();}// ...
Then it's all about the Observer (for this example, we will use Model Observer, but you can use a dedicated Observer if you want):
app/Models/Invoice.php
use App\Jobs\GenerateInvoiceNumberJob; // ... protected static function booted(){ parent::booted(); self::created(static function (Invoice $invoice) { dispatch(new GenerateInvoiceNumberJob($invoice->id)); });}// ...
Once you create an invoice, it will automatically schedule a job to generate the serial number. It does not matter where you will create it.
Code in Repository
Bonus: Adding Leading Zeros
It's very common to have leading zeros in your serial numbers. For example, you might want to have ABC-001
instead of ABC-1
. This is very easy to do like this:
app/Jobs/GenerateInvoiceNumberJob.php
public function handle(): void{ // ... $this->invoice->serial = $this->invoice->serial_series . '-' . $this->invoice->serial_number; $this->invoice->serial = $this->invoice->serial_series . '-' . str_pad($this->invoice->serial_number, 5, '0', STR_PAD_LEFT); // ...}
Or in short, here's how the str_pad()
works:
str_pad('1', 5, '0', STR_PAD_LEFT); // 00001
-
1
- The string we want to pad -
5
- The total length of the string we want to have -
0
- The character we want to use to pad the string -
STR_PAD_LEFT
- The side we want to pad the string on (left, right, both)
That's it! Now you have a serial number with leading zeros.
Is there a way to make it restart every year? 0001-2022 0002-2022 .... 0160-2022 0001-2023
yes!
Set your serial series to the year and that's it!
For example in config file set it to now("Y")
like this?
return [ 'availableInvoiceSeries' => [ now("Y"); ], ];
Yes, just without typos:
Of course, this will be the only selection in the dropdown, but you can make it a hidden field instead of select (if you don't need to select the series)
thank you very much for your help
Hello
Hi,
Verry usefull, i found this for primary, would this avoid using the job? I mean if some of the columns used is autoincrement. https://laravel.com/api/10.x/Illuminate/Database/Schema/Blueprint.html#method_primary
The main purpose of the job is not only to avoid the issue with repeating numbers, but to also solve the problem of re-trying the job.
Imagine, you have an index that's unique (mentioned in 3rd option) and when trying to create the invoice - it fails. Then you have to manually re-try the saving with a new number. Job does by itself.
Also, auto-increment is okay, but it would not be PER each of the series, so that causes other issues where gaps will appear. And gaps are pretty bad!
What about soft deleted records? how to deal with them? here:
You can add withTrashed() and that will cover it