|
@@ -0,0 +1,533 @@
|
|
1
|
+<?php
|
|
2
|
+
|
|
3
|
+namespace App\Models\Accounting;
|
|
4
|
+
|
|
5
|
+use App\Casts\MoneyCast;
|
|
6
|
+use App\Casts\RateCast;
|
|
7
|
+use App\Collections\Accounting\DocumentCollection;
|
|
8
|
+use App\Enums\Accounting\AdjustmentComputation;
|
|
9
|
+use App\Enums\Accounting\CreditNoteStatus;
|
|
10
|
+use App\Enums\Accounting\DocumentDiscountMethod;
|
|
11
|
+use App\Enums\Accounting\DocumentType;
|
|
12
|
+use App\Enums\Accounting\JournalEntryType;
|
|
13
|
+use App\Enums\Accounting\TransactionType;
|
|
14
|
+use App\Filament\Company\Resources\Sales\CreditNoteResource;
|
|
15
|
+use App\Models\Common\Client;
|
|
16
|
+use App\Models\Company;
|
|
17
|
+use App\Models\Setting\DocumentDefault;
|
|
18
|
+use App\Utilities\Currency\CurrencyAccessor;
|
|
19
|
+use App\Utilities\Currency\CurrencyConverter;
|
|
20
|
+use Filament\Actions\Action;
|
|
21
|
+use Filament\Actions\MountableAction;
|
|
22
|
+use Filament\Actions\ReplicateAction;
|
|
23
|
+use Illuminate\Database\Eloquent\Attributes\CollectedBy;
|
|
24
|
+use Illuminate\Database\Eloquent\Casts\Attribute;
|
|
25
|
+use Illuminate\Database\Eloquent\Model;
|
|
26
|
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
|
27
|
+use Illuminate\Database\Eloquent\Relations\MorphMany;
|
|
28
|
+use Illuminate\Database\Eloquent\Relations\MorphOne;
|
|
29
|
+use Illuminate\Support\Carbon;
|
|
30
|
+use Illuminate\Support\Collection;
|
|
31
|
+
|
|
32
|
+#[CollectedBy(DocumentCollection::class)]
|
|
33
|
+class CreditNote extends Document
|
|
34
|
+{
|
|
35
|
+ protected $table = 'credit_notes';
|
|
36
|
+
|
|
37
|
+ protected $fillable = [
|
|
38
|
+ 'company_id',
|
|
39
|
+ 'client_id',
|
|
40
|
+ 'logo',
|
|
41
|
+ 'header',
|
|
42
|
+ 'subheader',
|
|
43
|
+ 'credit_note_number',
|
|
44
|
+ 'reference_number',
|
|
45
|
+ 'date',
|
|
46
|
+ 'approved_at',
|
|
47
|
+ 'last_sent_at',
|
|
48
|
+ 'last_viewed_at',
|
|
49
|
+ 'status',
|
|
50
|
+ 'currency_code',
|
|
51
|
+ 'discount_method',
|
|
52
|
+ 'discount_computation',
|
|
53
|
+ 'discount_rate',
|
|
54
|
+ 'subtotal',
|
|
55
|
+ 'tax_total',
|
|
56
|
+ 'discount_total',
|
|
57
|
+ 'total',
|
|
58
|
+ 'amount_used',
|
|
59
|
+ 'terms',
|
|
60
|
+ 'footer',
|
|
61
|
+ 'created_by',
|
|
62
|
+ 'updated_by',
|
|
63
|
+ ];
|
|
64
|
+
|
|
65
|
+ protected $casts = [
|
|
66
|
+ 'date' => 'date',
|
|
67
|
+ 'approved_at' => 'datetime',
|
|
68
|
+ 'last_sent_at' => 'datetime',
|
|
69
|
+ 'last_viewed_at' => 'datetime',
|
|
70
|
+ 'status' => CreditNoteStatus::class,
|
|
71
|
+ 'discount_method' => DocumentDiscountMethod::class,
|
|
72
|
+ 'discount_computation' => AdjustmentComputation::class,
|
|
73
|
+ 'discount_rate' => RateCast::class,
|
|
74
|
+ 'subtotal' => MoneyCast::class,
|
|
75
|
+ 'tax_total' => MoneyCast::class,
|
|
76
|
+ 'discount_total' => MoneyCast::class,
|
|
77
|
+ 'total' => MoneyCast::class,
|
|
78
|
+ 'amount_used' => MoneyCast::class,
|
|
79
|
+ ];
|
|
80
|
+
|
|
81
|
+ // Basic Relationships
|
|
82
|
+
|
|
83
|
+ public function client(): BelongsTo
|
|
84
|
+ {
|
|
85
|
+ return $this->belongsTo(Client::class);
|
|
86
|
+ }
|
|
87
|
+
|
|
88
|
+ public function company(): BelongsTo
|
|
89
|
+ {
|
|
90
|
+ return $this->belongsTo(Company::class);
|
|
91
|
+ }
|
|
92
|
+
|
|
93
|
+ // Transaction Relationships
|
|
94
|
+
|
|
95
|
+ public function transactions(): MorphMany
|
|
96
|
+ {
|
|
97
|
+ return $this->morphMany(Transaction::class, 'transactionable');
|
|
98
|
+ }
|
|
99
|
+
|
|
100
|
+ public function initialTransaction(): MorphOne
|
|
101
|
+ {
|
|
102
|
+ return $this->morphOne(Transaction::class, 'transactionable')
|
|
103
|
+ ->where('type', TransactionType::Journal);
|
|
104
|
+ }
|
|
105
|
+
|
|
106
|
+ // Track where this credit note has been applied
|
|
107
|
+
|
|
108
|
+ public function applications(): Collection
|
|
109
|
+ {
|
|
110
|
+ // Find all invoice transactions that reference this credit note
|
|
111
|
+ return Transaction::where('type', TransactionType::CreditNote)
|
|
112
|
+ ->where('is_payment', true)
|
|
113
|
+ ->whereJsonContains('meta->credit_note_id', $this->id)
|
|
114
|
+ ->get()
|
|
115
|
+ ->map(function ($transaction) {
|
|
116
|
+ return [
|
|
117
|
+ 'invoice' => $transaction->transactionable,
|
|
118
|
+ 'amount' => $transaction->amount,
|
|
119
|
+ 'date' => $transaction->posted_at,
|
|
120
|
+ 'transaction' => $transaction,
|
|
121
|
+ ];
|
|
122
|
+ })
|
|
123
|
+ ->filter(function ($item) {
|
|
124
|
+ return ! is_null($item['invoice']);
|
|
125
|
+ });
|
|
126
|
+ }
|
|
127
|
+
|
|
128
|
+ public function appliedInvoices(): Collection
|
|
129
|
+ {
|
|
130
|
+ return $this->applications()->pluck('invoice');
|
|
131
|
+ }
|
|
132
|
+
|
|
133
|
+ // Document Interface Implementation
|
|
134
|
+
|
|
135
|
+ public static function documentType(): DocumentType
|
|
136
|
+ {
|
|
137
|
+ return DocumentType::CreditNote;
|
|
138
|
+ }
|
|
139
|
+
|
|
140
|
+ public function documentNumber(): ?string
|
|
141
|
+ {
|
|
142
|
+ return $this->credit_note_number;
|
|
143
|
+ }
|
|
144
|
+
|
|
145
|
+ public function documentDate(): ?string
|
|
146
|
+ {
|
|
147
|
+ return $this->date?->toDefaultDateFormat();
|
|
148
|
+ }
|
|
149
|
+
|
|
150
|
+ public function dueDate(): ?string
|
|
151
|
+ {
|
|
152
|
+ return null;
|
|
153
|
+ }
|
|
154
|
+
|
|
155
|
+ public function amountDue(): ?string
|
|
156
|
+ {
|
|
157
|
+ return null;
|
|
158
|
+ }
|
|
159
|
+
|
|
160
|
+ public function referenceNumber(): ?string
|
|
161
|
+ {
|
|
162
|
+ return $this->reference_number;
|
|
163
|
+ }
|
|
164
|
+
|
|
165
|
+ // Computed Properties
|
|
166
|
+
|
|
167
|
+ protected function availableBalance(): Attribute
|
|
168
|
+ {
|
|
169
|
+ return Attribute::get(function () {
|
|
170
|
+ $totalCents = (int) $this->getRawOriginal('total');
|
|
171
|
+ $amountUsedCents = (int) $this->getRawOriginal('amount_used');
|
|
172
|
+
|
|
173
|
+ return CurrencyConverter::convertCentsToFormatSimple($totalCents - $amountUsedCents);
|
|
174
|
+ });
|
|
175
|
+ }
|
|
176
|
+
|
|
177
|
+ protected function availableBalanceCents(): Attribute
|
|
178
|
+ {
|
|
179
|
+ return Attribute::get(function () {
|
|
180
|
+ $totalCents = (int) $this->getRawOriginal('total');
|
|
181
|
+ $amountUsedCents = (int) $this->getRawOriginal('amount_used');
|
|
182
|
+
|
|
183
|
+ return $totalCents - $amountUsedCents;
|
|
184
|
+ });
|
|
185
|
+ }
|
|
186
|
+
|
|
187
|
+ // Status Methods
|
|
188
|
+
|
|
189
|
+ public function isFullyApplied(): bool
|
|
190
|
+ {
|
|
191
|
+ return $this->availableBalanceCents <= 0;
|
|
192
|
+ }
|
|
193
|
+
|
|
194
|
+ public function isPartiallyApplied(): bool
|
|
195
|
+ {
|
|
196
|
+ $amountUsedCents = (int) $this->getRawOriginal('amount_used');
|
|
197
|
+
|
|
198
|
+ return $amountUsedCents > 0 && ! $this->isFullyApplied();
|
|
199
|
+ }
|
|
200
|
+
|
|
201
|
+ public function isDraft(): bool
|
|
202
|
+ {
|
|
203
|
+ return $this->status === CreditNoteStatus::Draft;
|
|
204
|
+ }
|
|
205
|
+
|
|
206
|
+ public function wasApproved(): bool
|
|
207
|
+ {
|
|
208
|
+ return $this->approved_at !== null;
|
|
209
|
+ }
|
|
210
|
+
|
|
211
|
+ public function hasBeenSent(): bool
|
|
212
|
+ {
|
|
213
|
+ return $this->last_sent_at !== null;
|
|
214
|
+ }
|
|
215
|
+
|
|
216
|
+ public function hasBeenViewed(): bool
|
|
217
|
+ {
|
|
218
|
+ return $this->last_viewed_at !== null;
|
|
219
|
+ }
|
|
220
|
+
|
|
221
|
+ public function canBeAppliedToInvoice(): bool
|
|
222
|
+ {
|
|
223
|
+ return in_array($this->status->value, CreditNoteStatus::canBeApplied()) &&
|
|
224
|
+ $this->availableBalanceCents > 0;
|
|
225
|
+ }
|
|
226
|
+
|
|
227
|
+ // Application Methods
|
|
228
|
+
|
|
229
|
+ public function applyToInvoice(Invoice $invoice, string $amount): void
|
|
230
|
+ {
|
|
231
|
+ // Validate currencies match
|
|
232
|
+ if ($this->currency_code !== $invoice->currency_code) {
|
|
233
|
+ throw new \RuntimeException('Cannot apply credit note with different currency to invoice.');
|
|
234
|
+ }
|
|
235
|
+
|
|
236
|
+ // Validate available amount
|
|
237
|
+ $amountCents = CurrencyConverter::convertToCents($amount, $this->currency_code);
|
|
238
|
+
|
|
239
|
+ if ($amountCents > $this->availableBalanceCents) {
|
|
240
|
+ throw new \RuntimeException('Cannot apply more than the available credit note amount.');
|
|
241
|
+ }
|
|
242
|
+
|
|
243
|
+ // Create transaction on the invoice
|
|
244
|
+ $invoice->transactions()->create([
|
|
245
|
+ 'company_id' => $this->company_id,
|
|
246
|
+ 'type' => TransactionType::CreditNote,
|
|
247
|
+ 'is_payment' => true,
|
|
248
|
+ 'posted_at' => now(),
|
|
249
|
+ 'amount' => $amount,
|
|
250
|
+ 'account_id' => Account::getAccountsReceivableAccount()->id,
|
|
251
|
+ 'description' => "Credit Note #{$this->credit_note_number} applied to Invoice #{$invoice->invoice_number}",
|
|
252
|
+ 'meta' => [
|
|
253
|
+ 'credit_note_id' => $this->id,
|
|
254
|
+ ],
|
|
255
|
+ ]);
|
|
256
|
+
|
|
257
|
+ // Update amount used on credit note
|
|
258
|
+ $this->amount_used = CurrencyConverter::convertCentsToFormatSimple(
|
|
259
|
+ (int) $this->getRawOriginal('amount_used') + $amountCents
|
|
260
|
+ );
|
|
261
|
+ $this->save();
|
|
262
|
+
|
|
263
|
+ // Update status if needed
|
|
264
|
+ $this->updateStatusBasedOnUsage();
|
|
265
|
+
|
|
266
|
+ // Update invoice payment status
|
|
267
|
+ $invoice->updatePaymentStatus();
|
|
268
|
+ }
|
|
269
|
+
|
|
270
|
+ protected function updateStatusBasedOnUsage(): void
|
|
271
|
+ {
|
|
272
|
+ if ($this->isFullyApplied()) {
|
|
273
|
+ $this->status = CreditNoteStatus::Applied;
|
|
274
|
+ } elseif ($this->isPartiallyApplied()) {
|
|
275
|
+ $this->status = CreditNoteStatus::Partial;
|
|
276
|
+ }
|
|
277
|
+
|
|
278
|
+ $this->save();
|
|
279
|
+ }
|
|
280
|
+
|
|
281
|
+ public function autoApplyToInvoices(): void
|
|
282
|
+ {
|
|
283
|
+ // Skip if no available amount
|
|
284
|
+ if ($this->availableBalanceCents <= 0) {
|
|
285
|
+ return;
|
|
286
|
+ }
|
|
287
|
+
|
|
288
|
+ // Find unpaid invoices for this client, ordered by due date (oldest first)
|
|
289
|
+ $unpaidInvoices = Invoice::where('client_id', $this->client_id)
|
|
290
|
+ ->where('currency_code', $this->currency_code)
|
|
291
|
+ ->unpaid()
|
|
292
|
+ ->orderBy('due_date')
|
|
293
|
+ ->get();
|
|
294
|
+
|
|
295
|
+ // Apply to invoices until amount is used up
|
|
296
|
+ foreach ($unpaidInvoices as $invoice) {
|
|
297
|
+ $invoiceAmountDueCents = (int) $invoice->getRawOriginal('amount_due');
|
|
298
|
+
|
|
299
|
+ if ($invoiceAmountDueCents <= 0 || $this->availableBalanceCents <= 0) {
|
|
300
|
+ continue;
|
|
301
|
+ }
|
|
302
|
+
|
|
303
|
+ // Calculate amount to apply to this invoice
|
|
304
|
+ $applyAmountCents = min($this->availableBalanceCents, $invoiceAmountDueCents);
|
|
305
|
+ $applyAmount = CurrencyConverter::convertCentsToFormatSimple($applyAmountCents);
|
|
306
|
+
|
|
307
|
+ // Apply to invoice
|
|
308
|
+ $this->applyToInvoice($invoice, $applyAmount);
|
|
309
|
+
|
|
310
|
+ if ($this->availableBalanceCents <= 0) {
|
|
311
|
+ break;
|
|
312
|
+ }
|
|
313
|
+ }
|
|
314
|
+ }
|
|
315
|
+
|
|
316
|
+ // Accounting Methods
|
|
317
|
+
|
|
318
|
+ public function createInitialTransaction(?Carbon $postedAt = null): void
|
|
319
|
+ {
|
|
320
|
+ $postedAt ??= $this->date;
|
|
321
|
+
|
|
322
|
+ $total = $this->formatAmountToDefaultCurrency($this->getRawOriginal('total'));
|
|
323
|
+
|
|
324
|
+ $transaction = $this->transactions()->create([
|
|
325
|
+ 'company_id' => $this->company_id,
|
|
326
|
+ 'type' => TransactionType::Journal,
|
|
327
|
+ 'posted_at' => $postedAt,
|
|
328
|
+ 'amount' => $total,
|
|
329
|
+ 'description' => 'Credit Note Creation for Credit Note #' . $this->credit_note_number,
|
|
330
|
+ ]);
|
|
331
|
+
|
|
332
|
+ $baseDescription = "{$this->client->name}: Credit Note #{$this->credit_note_number}";
|
|
333
|
+
|
|
334
|
+ // Credit AR (opposite of invoice)
|
|
335
|
+ $transaction->journalEntries()->create([
|
|
336
|
+ 'company_id' => $this->company_id,
|
|
337
|
+ 'type' => JournalEntryType::Credit,
|
|
338
|
+ 'account_id' => Account::getAccountsReceivableAccount()->id,
|
|
339
|
+ 'amount' => $total,
|
|
340
|
+ 'description' => $baseDescription,
|
|
341
|
+ ]);
|
|
342
|
+
|
|
343
|
+ // Handle line items - debit revenue accounts (reverse of invoice)
|
|
344
|
+ foreach ($this->lineItems as $lineItem) {
|
|
345
|
+ $lineItemDescription = "{$baseDescription} › {$lineItem->offering->name}";
|
|
346
|
+ $lineItemSubtotal = $this->formatAmountToDefaultCurrency($lineItem->getRawOriginal('subtotal'));
|
|
347
|
+
|
|
348
|
+ $transaction->journalEntries()->create([
|
|
349
|
+ 'company_id' => $this->company_id,
|
|
350
|
+ 'type' => JournalEntryType::Debit,
|
|
351
|
+ 'account_id' => $lineItem->offering->income_account_id,
|
|
352
|
+ 'amount' => $lineItemSubtotal,
|
|
353
|
+ 'description' => $lineItemDescription,
|
|
354
|
+ ]);
|
|
355
|
+
|
|
356
|
+ // Handle adjustments
|
|
357
|
+ foreach ($lineItem->adjustments as $adjustment) {
|
|
358
|
+ $adjustmentAmount = $this->formatAmountToDefaultCurrency($lineItem->calculateAdjustmentTotalAmount($adjustment));
|
|
359
|
+
|
|
360
|
+ $transaction->journalEntries()->create([
|
|
361
|
+ 'company_id' => $this->company_id,
|
|
362
|
+ 'type' => $adjustment->category->isDiscount() ? JournalEntryType::Credit : JournalEntryType::Debit,
|
|
363
|
+ 'account_id' => $adjustment->account_id,
|
|
364
|
+ 'amount' => $adjustmentAmount,
|
|
365
|
+ 'description' => $lineItemDescription,
|
|
366
|
+ ]);
|
|
367
|
+ }
|
|
368
|
+ }
|
|
369
|
+ }
|
|
370
|
+
|
|
371
|
+ public function approveDraft(?Carbon $approvedAt = null): void
|
|
372
|
+ {
|
|
373
|
+ if (! $this->isDraft()) {
|
|
374
|
+ throw new \RuntimeException('Credit note is not in draft status.');
|
|
375
|
+ }
|
|
376
|
+
|
|
377
|
+ $this->createInitialTransaction();
|
|
378
|
+
|
|
379
|
+ $approvedAt ??= now();
|
|
380
|
+
|
|
381
|
+ $this->update([
|
|
382
|
+ 'approved_at' => $approvedAt,
|
|
383
|
+ 'status' => CreditNoteStatus::Sent,
|
|
384
|
+ ]);
|
|
385
|
+
|
|
386
|
+ // Auto-apply if configured in settings
|
|
387
|
+ if ($this->company->settings->auto_apply_credit_notes ?? true) {
|
|
388
|
+ $this->autoApplyToInvoices();
|
|
389
|
+ }
|
|
390
|
+ }
|
|
391
|
+
|
|
392
|
+ public function convertAmountToDefaultCurrency(int $amountCents): int
|
|
393
|
+ {
|
|
394
|
+ $defaultCurrency = CurrencyAccessor::getDefaultCurrency();
|
|
395
|
+ $needsConversion = $this->currency_code !== $defaultCurrency;
|
|
396
|
+
|
|
397
|
+ if ($needsConversion) {
|
|
398
|
+ return CurrencyConverter::convertBalance($amountCents, $this->currency_code, $defaultCurrency);
|
|
399
|
+ }
|
|
400
|
+
|
|
401
|
+ return $amountCents;
|
|
402
|
+ }
|
|
403
|
+
|
|
404
|
+ public function formatAmountToDefaultCurrency(int $amountCents): string
|
|
405
|
+ {
|
|
406
|
+ $convertedCents = $this->convertAmountToDefaultCurrency($amountCents);
|
|
407
|
+
|
|
408
|
+ return CurrencyConverter::convertCentsToFormatSimple($convertedCents);
|
|
409
|
+ }
|
|
410
|
+
|
|
411
|
+ // Other methods
|
|
412
|
+
|
|
413
|
+ public function markAsSent(?Carbon $sentAt = null): void
|
|
414
|
+ {
|
|
415
|
+ $sentAt ??= now();
|
|
416
|
+
|
|
417
|
+ $this->update([
|
|
418
|
+ 'status' => CreditNoteStatus::Sent,
|
|
419
|
+ 'last_sent_at' => $sentAt,
|
|
420
|
+ ]);
|
|
421
|
+ }
|
|
422
|
+
|
|
423
|
+ public function markAsViewed(?Carbon $viewedAt = null): void
|
|
424
|
+ {
|
|
425
|
+ $viewedAt ??= now();
|
|
426
|
+
|
|
427
|
+ $this->update([
|
|
428
|
+ 'status' => CreditNoteStatus::Viewed,
|
|
429
|
+ 'last_viewed_at' => $viewedAt,
|
|
430
|
+ ]);
|
|
431
|
+ }
|
|
432
|
+
|
|
433
|
+ // Utility Methods
|
|
434
|
+
|
|
435
|
+ public static function getNextDocumentNumber(?Company $company = null): string
|
|
436
|
+ {
|
|
437
|
+ $company ??= auth()->user()?->currentCompany;
|
|
438
|
+
|
|
439
|
+ if (! $company) {
|
|
440
|
+ throw new \RuntimeException('No current company is set for the user.');
|
|
441
|
+ }
|
|
442
|
+
|
|
443
|
+ $defaultSettings = $company->defaultCreditNote;
|
|
444
|
+
|
|
445
|
+ $numberPrefix = $defaultSettings->number_prefix ?? 'CN-';
|
|
446
|
+
|
|
447
|
+ $latestDocument = static::query()
|
|
448
|
+ ->whereNotNull('credit_note_number')
|
|
449
|
+ ->latest('credit_note_number')
|
|
450
|
+ ->first();
|
|
451
|
+
|
|
452
|
+ $lastNumberNumericPart = $latestDocument
|
|
453
|
+ ? (int) substr($latestDocument->credit_note_number, strlen($numberPrefix))
|
|
454
|
+ : DocumentDefault::getBaseNumber();
|
|
455
|
+
|
|
456
|
+ $numberNext = $lastNumberNumericPart + 1;
|
|
457
|
+
|
|
458
|
+ return $defaultSettings->getNumberNext(
|
|
459
|
+ prefix: $numberPrefix,
|
|
460
|
+ next: $numberNext
|
|
461
|
+ );
|
|
462
|
+ }
|
|
463
|
+
|
|
464
|
+ // Action Methods
|
|
465
|
+
|
|
466
|
+ public static function getReplicateAction(string $action = ReplicateAction::class): MountableAction
|
|
467
|
+ {
|
|
468
|
+ return $action::make()
|
|
469
|
+ ->excludeAttributes([
|
|
470
|
+ 'status',
|
|
471
|
+ 'amount_used',
|
|
472
|
+ 'created_by',
|
|
473
|
+ 'updated_by',
|
|
474
|
+ 'created_at',
|
|
475
|
+ 'updated_at',
|
|
476
|
+ 'credit_note_number',
|
|
477
|
+ 'date',
|
|
478
|
+ 'approved_at',
|
|
479
|
+ 'last_sent_at',
|
|
480
|
+ 'last_viewed_at',
|
|
481
|
+ ])
|
|
482
|
+ ->modal(false)
|
|
483
|
+ ->beforeReplicaSaved(function (self $original, self $replica) {
|
|
484
|
+ $replica->status = CreditNoteStatus::Draft;
|
|
485
|
+ $replica->credit_note_number = self::getNextDocumentNumber();
|
|
486
|
+ $replica->date = now();
|
|
487
|
+ $replica->amount_used = 0;
|
|
488
|
+ })
|
|
489
|
+ ->databaseTransaction()
|
|
490
|
+ ->after(function (self $original, self $replica) {
|
|
491
|
+ $original->replicateLineItems($replica);
|
|
492
|
+ })
|
|
493
|
+ ->successRedirectUrl(static function (self $replica) {
|
|
494
|
+ return CreditNoteResource::getUrl('edit', ['record' => $replica]);
|
|
495
|
+ });
|
|
496
|
+ }
|
|
497
|
+
|
|
498
|
+ public static function getApproveAction(string $action = Action::class): MountableAction
|
|
499
|
+ {
|
|
500
|
+ return $action::make('approve')
|
|
501
|
+ ->label('Approve')
|
|
502
|
+ ->icon('heroicon-m-check-circle')
|
|
503
|
+ ->visible(fn (self $record) => $record->isDraft())
|
|
504
|
+ ->requiresConfirmation()
|
|
505
|
+ ->databaseTransaction()
|
|
506
|
+ ->successNotificationTitle('Credit note approved')
|
|
507
|
+ ->action(function (self $record) {
|
|
508
|
+ $record->approveDraft();
|
|
509
|
+ });
|
|
510
|
+ }
|
|
511
|
+
|
|
512
|
+ public function replicateLineItems(Model $target): void
|
|
513
|
+ {
|
|
514
|
+ $this->lineItems->each(function (DocumentLineItem $lineItem) use ($target) {
|
|
515
|
+ $replica = $lineItem->replicate([
|
|
516
|
+ 'documentable_id',
|
|
517
|
+ 'documentable_type',
|
|
518
|
+ 'subtotal',
|
|
519
|
+ 'total',
|
|
520
|
+ 'created_by',
|
|
521
|
+ 'updated_by',
|
|
522
|
+ 'created_at',
|
|
523
|
+ 'updated_at',
|
|
524
|
+ ]);
|
|
525
|
+
|
|
526
|
+ $replica->documentable_id = $target->id;
|
|
527
|
+ $replica->documentable_type = $target->getMorphClass();
|
|
528
|
+ $replica->save();
|
|
529
|
+
|
|
530
|
+ $replica->adjustments()->sync($lineItem->adjustments->pluck('id'));
|
|
531
|
+ });
|
|
532
|
+ }
|
|
533
|
+}
|