'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']); } }