sackey e3b2b220d2
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run
Enhance UI and functionality across various components
- Increased icon sizes in service items, service orders, users, and technician management for better visibility.
- Added custom loading indicators with appropriate icons in search fields for vehicles, work orders, and technicians.
- Introduced invoice management routes for better organization and access control.
- Created a new test for the estimate PDF functionality to ensure proper rendering and data integrity.
2025-08-16 14:36:58 +00:00

269 lines
6.2 KiB
PHP

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Invoice extends Model
{
use HasFactory;
protected $fillable = [
'invoice_number',
'status',
'customer_id',
'service_order_id',
'job_card_id',
'estimate_id',
'branch_id',
'created_by',
'invoice_date',
'due_date',
'description',
'notes',
'terms_and_conditions',
'subtotal',
'tax_rate',
'tax_amount',
'discount_amount',
'total_amount',
'paid_at',
'payment_method',
'payment_reference',
'payment_notes',
'sent_at',
'sent_method',
'sent_to',
];
protected $casts = [
'invoice_date' => 'date',
'due_date' => 'date',
'paid_at' => 'datetime',
'sent_at' => 'datetime',
'subtotal' => 'decimal:2',
'tax_rate' => 'decimal:2',
'tax_amount' => 'decimal:2',
'discount_amount' => 'decimal:2',
'total_amount' => 'decimal:2',
];
/**
* Generate the next invoice number for a branch
*/
public static function generateInvoiceNumber(string $branchCode = 'MAIN'): string
{
$year = now()->year;
$prefix = strtoupper($branchCode).'-INV-'.$year.'-';
$lastInvoice = static::where('invoice_number', 'like', $prefix.'%')
->orderBy('invoice_number', 'desc')
->first();
if ($lastInvoice) {
$lastNumber = (int) substr($lastInvoice->invoice_number, strlen($prefix));
$nextNumber = $lastNumber + 1;
} else {
$nextNumber = 1;
}
return $prefix.str_pad($nextNumber, 5, '0', STR_PAD_LEFT);
}
/**
* Status options for invoices
*/
public static function getStatusOptions(): array
{
return [
'draft' => 'Draft',
'sent' => 'Sent',
'paid' => 'Paid',
'overdue' => 'Overdue',
'cancelled' => 'Cancelled',
];
}
/**
* Payment method options
*/
public static function getPaymentMethodOptions(): array
{
return [
'cash' => 'Cash',
'card' => 'Credit/Debit Card',
'check' => 'Check',
'bank_transfer' => 'Bank Transfer',
'other' => 'Other',
];
}
/**
* Check if invoice is overdue
*/
public function isOverdue(): bool
{
return $this->status !== 'paid' &&
$this->status !== 'cancelled' &&
$this->due_date < now()->startOfDay();
}
/**
* Check if invoice is paid
*/
public function isPaid(): bool
{
return $this->status === 'paid' && ! is_null($this->paid_at);
}
/**
* Mark invoice as paid
*/
public function markAsPaid(?string $paymentMethod = null, ?string $reference = null, ?string $notes = null): void
{
$this->update([
'status' => 'paid',
'paid_at' => now(),
'payment_method' => $paymentMethod,
'payment_reference' => $reference,
'payment_notes' => $notes,
]);
}
/**
* Mark invoice as sent
*/
public function markAsSent(string $method = 'email', ?string $sentTo = null): void
{
$this->update([
'status' => 'sent',
'sent_at' => now(),
'sent_method' => $method,
'sent_to' => $sentTo,
]);
}
/**
* Calculate totals from line items
*/
public function recalculateTotals(): void
{
$subtotal = $this->lineItems()->sum('total_amount');
$taxAmount = $subtotal * ($this->tax_rate / 100);
$total = $subtotal + $taxAmount - $this->discount_amount;
$this->update([
'subtotal' => $subtotal,
'tax_amount' => $taxAmount,
'total_amount' => $total,
]);
}
// Relationships
/**
* Invoice belongs to a customer
*/
public function customer(): BelongsTo
{
return $this->belongsTo(Customer::class);
}
/**
* Invoice belongs to a branch
*/
public function branch(): BelongsTo
{
return $this->belongsTo(Branch::class);
}
/**
* Invoice belongs to a user (creator)
*/
public function createdBy(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
/**
* Invoice may belong to a service order
*/
public function serviceOrder(): BelongsTo
{
return $this->belongsTo(ServiceOrder::class);
}
/**
* Invoice may belong to a job card
*/
public function jobCard(): BelongsTo
{
return $this->belongsTo(JobCard::class);
}
/**
* Invoice may belong to an estimate
*/
public function estimate(): BelongsTo
{
return $this->belongsTo(Estimate::class);
}
/**
* Invoice has many line items
*/
public function lineItems(): HasMany
{
return $this->hasMany(InvoiceLineItem::class);
}
// Scopes
/**
* Scope to filter by branch
*/
public function scopeByBranch($query, string $branchCode)
{
return $query->whereHas('branch', function ($q) use ($branchCode) {
$q->where('code', $branchCode);
});
}
/**
* Scope to filter by status
*/
public function scopeByStatus($query, string $status)
{
return $query->where('status', $status);
}
/**
* Scope to get overdue invoices
*/
public function scopeOverdue($query)
{
return $query->where('status', '!=', 'paid')
->where('status', '!=', 'cancelled')
->where('due_date', '<', now()->startOfDay());
}
/**
* Scope to get paid invoices
*/
public function scopePaid($query)
{
return $query->where('status', 'paid');
}
/**
* Scope to get unpaid invoices
*/
public function scopeUnpaid($query)
{
return $query->whereIn('status', ['draft', 'sent', 'overdue']);
}
}