Skip to content

Commit

Permalink
Merge pull request #362 from liberu-accounting/sweep/Enhance-Invoice-…
Browse files Browse the repository at this point in the history
…Management-with-PDF-Generation-and-Improved-Filament-Resource

Enhance Invoice Management with PDF Generation and Improved Filament Resource
  • Loading branch information
curtisdelicata authored Dec 24, 2024
2 parents ffa4f48 + bcc0259 commit 9276f7f
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 122 deletions.
149 changes: 67 additions & 82 deletions app/Filament/App/Resources/InvoiceResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,122 +12,107 @@
use Filament\Tables\Columns\TextColumn;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\DatePicker;
use Filament\Forms\Components\Textarea;
use Illuminate\Database\Eloquent\Builder;
use Filament\Forms\Components\BelongsToSelect;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Filament\Tables\Actions\Action;
use App\Filament\App\Resources\InvoiceResource\Pages;
use App\Filament\App\Resources\InvoiceResource\RelationManagers;

class InvoiceResource extends Resource
{
protected static ?string $model = Invoice::class;

protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
protected static ?string $navigationIcon = 'heroicon-o-document-text';

public static function form(Form $form): Form
{
return $form
->schema([
Grid::make(2)->schema([
BelongsToSelect::make('customer_id')
->relationship('customer', 'customer_name')
->label('Customer')
->columnSpan(1),
DatePicker::make('invoice_date')
->columnSpan(1),
DatePicker::make('due_date')
->columnSpan(1),
TextInput::make('total_amount')
->numeric()
->reactive()
->afterStateUpdated(function ($state, callable $set, $get) {
if ($get('tax_rate_id')) {
$taxRate = TaxRate::find($get('tax_rate_id'));
$taxAmount = $state * ($taxRate->rate / 100);
$set('tax_amount', $taxAmount);
}
})
->columnSpan(1),
BelongsToSelect::make('tax_rate_id')
->relationship('taxRate', 'name')
->label('Tax Rate')
->reactive()
->afterStateUpdated(function ($state, callable $set, $get) {
if ($state && $get('total_amount')) {
$taxRate = TaxRate::find($state);
$taxAmount = $get('total_amount') * ($taxRate->rate / 100);
$set('tax_amount', $taxAmount);
}
})
->columnSpan(1),
TextInput::make('tax_amount')
->numeric()
->disabled()
->label('Tax Amount')
->columnSpan(1),
TextInput::make('late_fee_percentage')
->numeric()
->label('Late Fee (%)')
->default(0)
->columnSpan(1),
TextInput::make('grace_period_days')
->numeric()
->label('Grace Period (Days)')
->default(0)
->columnSpan(1),
TextInput::make('late_fee_amount')
->disabled()
->numeric()
->label('Late Fee Amount')
->columnSpan(1),
Select::make('payment_status')
->options([
'pending' => 'Pending',
'paid' => 'Paid',
'failed' => 'Failed',
])
->label('Payment Status')
->columnSpan(1),
]),
BelongsToSelect::make('customer_id')
->relationship('customer', 'customer_name')
->required()
->searchable(),
TextInput::make('invoice_number')
->disabled()
->dehydrated(false)
->visible(fn ($record) => $record !== null),
DatePicker::make('invoice_date')
->required(),
DatePicker::make('due_date'),
TextInput::make('total_amount')
->numeric()
->required()
->reactive()
->afterStateUpdated(function ($state, callable $set, $get) {
if ($get('tax_rate_id')) {
$taxRate = \App\Models\TaxRate::find($get('tax_rate_id'));
$taxAmount = $state * ($taxRate->rate / 100);
$set('tax_amount', $taxAmount);
}
}),
BelongsToSelect::make('tax_rate_id')
->relationship('taxRate', 'name')
->reactive()
->afterStateUpdated(function ($state, callable $set, $get) {
if ($state && $get('total_amount')) {
$taxRate = \App\Models\TaxRate::find($state);
$taxAmount = $get('total_amount') * ($taxRate->rate / 100);
$set('tax_amount', $taxAmount);
}
}),
TextInput::make('tax_amount')
->numeric()
->disabled(),
Select::make('payment_status')
->options([
'pending' => 'Pending',
'paid' => 'Paid',
'failed' => 'Failed',
])
->required(),
Textarea::make('notes')
->rows(3),
]);
}

public static function table(Table $table): Table
{
return $table
->columns([
TextColumn::make('customer_id')
->label('Customer')
TextColumn::make('invoice_number')
->searchable()
->sortable(),
TextColumn::make('invoice_date')
->label('Invoice Date')
TextColumn::make('customer.customer_name')
->searchable()
->sortable(),
TextColumn::make('invoice_date')
->date()
->sortable(),
TextColumn::make('total_amount')
->label('Total Amount')
->searchable()
->money('USD')
->sortable(),
TextColumn::make('payment_status'),
])
->filters([
//
TextColumn::make('payment_status')
->badge()
->color(fn (string $state): string => match ($state) {
'paid' => 'success',
'failed' => 'danger',
default => 'warning',
}),
])
->actions([
Tables\Actions\EditAction::make(),
])
->bulkActions([
Tables\Actions\BulkActionGroup::make([
Tables\Actions\DeleteBulkAction::make(),
]),
Action::make('download')
->icon('heroicon-o-document-download')
->action(fn (Invoice $record) => response()->streamDownload(
fn () => print($record->generatePDF()),
"invoice_{$record->invoice_number}.pdf"
)),
]);
}

public static function getRelations(): array
{
return [
//
];
return [];
}

public static function getPages(): array
Expand Down
66 changes: 26 additions & 40 deletions app/Models/Invoice.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

namespace App\Models;

use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Barryvdh\DomPDF\Facade\Pdf;

class Invoice extends Model
{
Expand All @@ -14,23 +14,20 @@ class Invoice extends Model

protected $fillable = [
"customer_id",
"invoice_number",
"invoice_date",
"due_date",
"total_amount",
"tax_amount",
"tax_rate_id",
"payment_status",
"late_fee_percentage",
"grace_period_days",
"late_fee_amount"
"notes"
];

protected $casts = [
'total_amount' => 'decimal:2',
'tax_amount' => 'decimal:2',
'late_fee_amount' => 'decimal:2',
'late_fee_percentage' => 'decimal:2',
'invoice_date' => 'datetime',
'invoice_date' => 'date',
'due_date' => 'date',
];

Expand Down Expand Up @@ -60,48 +57,37 @@ public function calculateTax()
return $taxAmount;
}

public function calculateLateFee()
{
if ($this->payment_status === 'paid' || !$this->due_date) {
return 0;
}

$dueDate = Carbon::parse($this->due_date)->addDays($this->grace_period_days);
$today = Carbon::now();

if ($today->lte($dueDate)) {
return 0;
}

$lateFee = $this->total_amount * ($this->late_fee_percentage / 100);
$this->late_fee_amount = $lateFee;
$this->save();

return $lateFee;
}

public function isOverdue()
public function getTotalWithTax()
{
if (!$this->due_date || $this->payment_status === 'paid') {
return false;
}

return Carbon::now()->gt(Carbon::parse($this->due_date)->addDays($this->grace_period_days));
return $this->total_amount + $this->tax_amount;
}

public function getTotalWithTax()
public function calculateTotalFromTimeEntries()
{
return $this->total_amount + $this->tax_amount;
$this->total_amount = $this->timeEntries->sum('total_amount');
return $this->total_amount;
}

public function getTotalWithTaxAndLateFees()
public function generatePDF()
{
return $this->getTotalWithTax() + $this->late_fee_amount;
$data = [
'invoice' => $this,
'customer' => $this->customer,
'tax_rate' => $this->taxRate,
];

$pdf = PDF::loadView('invoices.template', $data);
return $pdf->download('invoice_' . $this->invoice_number . '.pdf');
}

public function calculateTotalFromTimeEntries()
protected static function boot()
{
$this->total_amount = $this->timeEntries->sum('total_amount');
return $this->total_amount;
parent::boot();

static::creating(function ($invoice) {
if (empty($invoice->invoice_number)) {
$invoice->invoice_number = 'INV-' . str_pad(static::max('invoice_id') + 1, 6, '0', STR_PAD_LEFT);
}
});
}
}
32 changes: 32 additions & 0 deletions database/migrations/2024_02_20_create_invoices_table.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@


<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
public function up()
{
Schema::create('invoices', function (Blueprint $table) {
$table->id('invoice_id');
$table->foreignId('customer_id')->constrained('customers');
$table->string('invoice_number')->unique();
$table->date('invoice_date');
$table->date('due_date')->nullable();
$table->decimal('total_amount', 10, 2)->default(0);
$table->decimal('tax_amount', 10, 2)->default(0);
$table->foreignId('tax_rate_id')->nullable()->constrained('tax_rates');
$table->enum('payment_status', ['pending', 'paid', 'failed'])->default('pending');
$table->text('notes')->nullable();
$table->timestamps();
});
}

public function down()
{
Schema::dropIfExists('invoices');
}
};
49 changes: 49 additions & 0 deletions resources/views/invoices/template.blade.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@


<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Invoice {{ $invoice->invoice_number }}</title>
<style>
body { font-family: Arial, sans-serif; }
.header { text-align: center; margin-bottom: 30px; }
.invoice-info { margin-bottom: 20px; }
.customer-info { margin-bottom: 20px; }
.amounts { margin-top: 20px; }
.total { font-weight: bold; }
</style>
</head>
<body>
<div class="header">
<h1>INVOICE</h1>
</div>

<div class="invoice-info">
<p><strong>Invoice Number:</strong> {{ $invoice->invoice_number }}</p>
<p><strong>Date:</strong> {{ $invoice->invoice_date->format('Y-m-d') }}</p>
<p><strong>Due Date:</strong> {{ $invoice->due_date ? $invoice->due_date->format('Y-m-d') : 'N/A' }}</p>
</div>

<div class="customer-info">
<h3>Bill To:</h3>
<p>{{ $customer->customer_name }}</p>
<p>{{ $customer->address ?? '' }}</p>
<p>{{ $customer->email }}</p>
</div>

<div class="amounts">
<p><strong>Subtotal:</strong> ${{ number_format($invoice->total_amount, 2) }}</p>
<p><strong>Tax Rate:</strong> {{ $tax_rate->rate ?? 0 }}%</p>
<p><strong>Tax Amount:</strong> ${{ number_format($invoice->tax_amount, 2) }}</p>
<p class="total"><strong>Total Amount:</strong> ${{ number_format($invoice->getTotalWithTax(), 2) }}</p>
</div>

@if($invoice->notes)
<div class="notes">
<h3>Notes:</h3>
<p>{{ $invoice->notes }}</p>
</div>
@endif
</body>
</html>

0 comments on commit 9276f7f

Please sign in to comment.