diff --git a/app/Http/Controllers/InvoiceController.php b/app/Http/Controllers/InvoiceController.php index bf90314..8e02735 100644 --- a/app/Http/Controllers/InvoiceController.php +++ b/app/Http/Controllers/InvoiceController.php @@ -56,6 +56,8 @@ class InvoiceController extends Controller 'items.*.quantity' => 'required|integer|min:1', 'items.*.unit_price' => 'required|numeric|min:0', 'items.*.is_taxable' => 'nullable|boolean', + 'items.*.is_discountable' => 'nullable|boolean', + 'discount_rate' => 'nullable|integer|min:0|max:100', ]); $items = $validated['items']; @@ -118,6 +120,8 @@ class InvoiceController extends Controller 'items.*.quantity' => 'required|integer|min:1', 'items.*.unit_price' => 'required|numeric|min:0', 'items.*.is_taxable' => 'nullable|boolean', + 'items.*.is_discountable' => 'nullable|boolean', + 'discount_rate' => 'nullable|integer|min:0|max:100', ]); $items = $validated['items']; @@ -151,4 +155,9 @@ class InvoiceController extends Controller { return $this->invoiceService->generatePdf($invoice); } + + public function pdfLegacy(Invoice $invoice): Response + { + return $this->invoiceService->generateLegacyPdf($invoice); + } } diff --git a/app/Models/Invoice.php b/app/Models/Invoice.php index 0551896..088ac33 100644 --- a/app/Models/Invoice.php +++ b/app/Models/Invoice.php @@ -21,7 +21,7 @@ class Invoice extends Model 'subtotal', 'tax_rate', 'tax_amount', 'total', 'currency', 'status', 'issue_date', 'due_date', 'paid_date', 'notes', 'bank_name', 'bank_account', 'bank_holder', 'branch_name', - 'signature_image', 'signer_name', 'signer_title', + 'signature_image', 'signer_name', 'signer_title', 'discount_rate', ]; protected $casts = [ @@ -47,14 +47,32 @@ class Invoice extends Model public function recalculate(): void { - // Total of all items (what the creator receives) + // Total of all items $this->subtotal = $this->items->sum('total'); - // Tax is calculated only on taxable items, but paid by the client on top - $taxableSubtotal = $this->items->where('is_taxable', true)->sum('total'); + $discountAmount = 0; + if ($this->discount_rate > 0) { + $discountableSubtotal = $this->items->where('is_discountable', true)->sum('total'); + $discountAmount = round((float) $discountableSubtotal * ($this->discount_rate / 100), 2); + } + + $this->subtotal = $this->subtotal - $discountAmount; + + // Tax is calculated only on taxable items + $taxableSubtotal = 0; + foreach ($this->items as $item) { + if ($item->is_taxable) { + if ($this->discount_rate > 0 && $item->is_discountable) { + $taxableSubtotal += $item->total * (1 - $this->discount_rate / 100); + } else { + $taxableSubtotal += $item->total; + } + } + } + $this->tax_amount = round((float) $taxableSubtotal * ($this->tax_rate / 100), 2); - // Grand total = subtotal (creator's fee, pre-tax β tax is borne by client separately) + // Grand total = subtotal (tax is borne by client separately) $this->total = $this->subtotal; $this->save(); } @@ -64,6 +82,14 @@ class Invoice extends Model return 'Rp ' . number_format((float) $this->total, 0, ',', '.'); } + public function getDiscountAmountAttribute(): float + { + if ($this->discount_rate <= 0) return 0.0; + + $discountableSubtotal = $this->items->where('is_discountable', true)->sum('total'); + return round((float) $discountableSubtotal * ($this->discount_rate / 100), 2); + } + public static function generateNumber(): string { return InvoiceSettings::generateNumber(); diff --git a/app/Models/InvoiceItem.php b/app/Models/InvoiceItem.php index 7affe3c..b0157f0 100644 --- a/app/Models/InvoiceItem.php +++ b/app/Models/InvoiceItem.php @@ -9,12 +9,13 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; class InvoiceItem extends Model { - protected $fillable = ['invoice_id', 'description', 'quantity', 'unit_price', 'total', 'is_taxable']; + protected $fillable = ['invoice_id', 'description', 'quantity', 'unit_price', 'total', 'is_taxable', 'is_discountable']; protected $casts = [ - 'unit_price' => 'decimal:2', - 'total' => 'decimal:2', - 'is_taxable' => 'boolean', + 'unit_price' => 'decimal:2', + 'total' => 'decimal:2', + 'is_taxable' => 'boolean', + 'is_discountable' => 'boolean', ]; public function invoice(): BelongsTo diff --git a/app/Services/InvoiceService.php b/app/Services/InvoiceService.php index bcb1ccd..a9e8815 100644 --- a/app/Services/InvoiceService.php +++ b/app/Services/InvoiceService.php @@ -59,4 +59,15 @@ final class InvoiceService return $pdf->download("invoice-{$invoice->invoice_number}.pdf"); } + + public function generateLegacyPdf(Invoice $invoice): Response + { + $invoice->load('client', 'items'); + $invoiceSettings = InvoiceSettings::instance(); + + $pdf = Pdf::loadView('invoices.pdf-legacy', compact('invoice', 'invoiceSettings')); + $pdf->setPaper('A4', 'portrait'); + + return $pdf->download("invoice-{$invoice->invoice_number}-legacy.pdf"); + } } diff --git a/database/migrations/2026_04_06_152600_add_discount_fields.php b/database/migrations/2026_04_06_152600_add_discount_fields.php new file mode 100644 index 0000000..e7fd0ee --- /dev/null +++ b/database/migrations/2026_04_06_152600_add_discount_fields.php @@ -0,0 +1,38 @@ +integer('discount_rate')->default(0); + } + }); + + Schema::table('invoice_items', function (Blueprint $table) { + if (!Schema::hasColumn('invoice_items', 'is_discountable')) { + $table->boolean('is_discountable')->default(true); + } + }); + } + + public function down(): void + { + Schema::table('invoices', function (Blueprint $table) { + if (Schema::hasColumn('invoices', 'discount_rate')) { + $table->dropColumn('discount_rate'); + } + }); + + Schema::table('invoice_items', function (Blueprint $table) { + if (Schema::hasColumn('invoice_items', 'is_discountable')) { + $table->dropColumn('is_discountable'); + } + }); + } +}; diff --git a/resources/views/invoices/create.blade.php b/resources/views/invoices/create.blade.php index 463a250..4774a0b 100644 --- a/resources/views/invoices/create.blade.php +++ b/resources/views/invoices/create.blade.php @@ -98,16 +98,28 @@ - {{-- Taxable checkbox --}} -
| + + | +
+ I N V O I C E
+ |
+
|
+ BILLING TO:
+ @if($invoice->client)
+ {{ $invoice->client->company }}
+
+ @if($invoice->client->full_address)
+ {!! nl2br(e($invoice->client->full_address)) !!} @endif
+ @else
+ β
+ @endif
+ |
+
+
|
+
| DESCRIPTION | +PRICE | +QTY | +TOTAL | +
|---|---|---|---|
| + {{ $item->description }} + @if(!$item->is_taxable) + Non-Taxable + @endif + @if($invoice->discount_rate > 0 && !$item->is_discountable) + No + Discount + @endif + | +Rp {{ number_format($item->unit_price, 0, ',', '.') }} | +{{ $item->quantity }} | +Rp {{ number_format($item->total, 0, ',', '.') }} | +
| SUBTOTAL | +Rp + {{ number_format($invoice->subtotal + $invoice->discount_amount, 0, ',', '.') }} + | +
| DISCOUNT ({{ $invoice->discount_rate }}%) | +- Rp + {{ number_format($invoice->discount_amount, 0, ',', '.') }} + | +
| TAX ({{ $invoice->tax_rate }}%) | +Rp + {{ number_format($invoice->tax_amount, 0, ',', '.') }} + | +
| GRAND TOTAL | +{{ $invoice->formatted_total }} | +
+ {{ $invoice->bank_name }}
+ A/N {{ $invoice->bank_holder }}
+ {{ $invoice->bank_account }}
+ @if($invoice->branch_name)
KCP: {{ $invoice->branch_name }}@endif
+
| Subtotal | -Rp {{ number_format($invoice->subtotal, 0, ',', '.') }} | +Rp {{ number_format($invoice->subtotal + $invoice->discount_amount, 0, ',', '.') }} |
| Discount ({{ $invoice->discount_rate }}%) | +- Rp {{ number_format($invoice->discount_amount, 0, ',', '.') }} | +|
| Tax ({{ $invoice->tax_rate }}% β paid by client) | diff --git a/resources/views/invoices/show.blade.php b/resources/views/invoices/show.blade.php index 6263ba4..5f7ba34 100644 --- a/resources/views/invoices/show.blade.php +++ b/resources/views/invoices/show.blade.php @@ -11,6 +11,9 @@ π₯ Download PDF + π₯ + PDF (Legacy)