- 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.
269 lines
6.2 KiB
PHP
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']);
|
|
}
|
|
}
|