Andrew Wallo 10 months ago
parent
commit
4d140ff66b

+ 3
- 1
app/Enums/Accounting/InvoiceStatus.php View File

@@ -17,6 +17,8 @@ enum InvoiceStatus: string implements HasColor, HasLabel
17 17
 
18 18
     case Overdue = 'overdue';
19 19
 
20
+    case Overpaid = 'overpaid';
21
+
20 22
     case Void = 'void';
21 23
 
22 24
     public function getLabel(): ?string
@@ -30,7 +32,7 @@ enum InvoiceStatus: string implements HasColor, HasLabel
30 32
             self::Draft, self::Unsent, self::Void => 'gray',
31 33
             self::Sent => 'primary',
32 34
             self::Partial => 'warning',
33
-            self::Paid => 'success',
35
+            self::Paid, self::Overpaid => 'success',
34 36
             self::Overdue => 'danger',
35 37
         };
36 38
     }

+ 130
- 2
app/Filament/Company/Resources/Sales/InvoiceResource.php View File

@@ -2,12 +2,15 @@
2 2
 
3 3
 namespace App\Filament\Company\Resources\Sales;
4 4
 
5
+use App\Enums\Accounting\InvoiceStatus;
5 6
 use App\Filament\Company\Resources\Sales\InvoiceResource\Pages;
6 7
 use App\Models\Accounting\Adjustment;
7 8
 use App\Models\Accounting\DocumentLineItem;
8 9
 use App\Models\Accounting\Invoice;
10
+use App\Models\Banking\BankAccount;
9 11
 use App\Models\Common\Offering;
10 12
 use App\Utilities\Currency\CurrencyAccessor;
13
+use App\Utilities\Currency\CurrencyConverter;
11 14
 use Awcodes\TableRepeater\Components\TableRepeater;
12 15
 use Awcodes\TableRepeater\Header;
13 16
 use Carbon\CarbonInterface;
@@ -439,8 +442,133 @@ class InvoiceResource extends Resource
439 442
                 //
440 443
             ])
441 444
             ->actions([
442
-                Tables\Actions\EditAction::make(),
443
-                Tables\Actions\ViewAction::make(),
445
+                Tables\Actions\ActionGroup::make([
446
+                    Tables\Actions\EditAction::make(),
447
+                    Tables\Actions\ViewAction::make(),
448
+                    Tables\Actions\DeleteAction::make(),
449
+                    Tables\Actions\ReplicateAction::make()
450
+                        ->label('Duplicate')
451
+                        ->excludeAttributes(['status', 'amount_paid', 'amount_due', 'created_by', 'updated_by', 'created_at', 'updated_at', 'invoice_number', 'date', 'due_date'])
452
+                        ->modal(false)
453
+                        ->beforeReplicaSaved(function (Invoice $original, Invoice $replica) {
454
+                            $replica->status = InvoiceStatus::Draft;
455
+                            $replica->invoice_number = Invoice::getNextDocumentNumber();
456
+                            $replica->date = now();
457
+                            $replica->due_date = now()->addDays($original->company->defaultInvoice->payment_terms->getDays());
458
+                        })
459
+                        ->after(function (Invoice $original, Invoice $replica) {
460
+                            $original->lineItems->each(function (DocumentLineItem $lineItem) use ($replica) {
461
+                                $replicaLineItem = $lineItem->replicate([
462
+                                    'documentable_id',
463
+                                    'documentable_type',
464
+                                    'subtotal',
465
+                                    'total',
466
+                                    'created_by',
467
+                                    'updated_by',
468
+                                    'created_at',
469
+                                    'updated_at',
470
+                                ]);
471
+
472
+                                $replicaLineItem->documentable_id = $replica->id;
473
+                                $replicaLineItem->documentable_type = $replica->getMorphClass();
474
+
475
+                                $replicaLineItem->save();
476
+
477
+                                $replicaLineItem->adjustments()->sync($lineItem->adjustments->pluck('id'));
478
+                            });
479
+                        })
480
+                        ->successRedirectUrl(function (Invoice $replica) {
481
+                            return InvoiceResource::getUrl('edit', ['record' => $replica]);
482
+                        }),
483
+                    Tables\Actions\Action::make('approveDraft')
484
+                        ->label('Approve')
485
+                        ->icon('heroicon-o-check-circle')
486
+                        ->visible(function (Invoice $record) {
487
+                            return $record->isDraft();
488
+                        })
489
+                        ->action(function (Invoice $record) {
490
+                            $record->updateQuietly([
491
+                                'status' => InvoiceStatus::Unsent,
492
+                            ]);
493
+                        }),
494
+                    Tables\Actions\Action::make('markAsSent')
495
+                        ->label('Mark as Sent')
496
+                        ->icon('heroicon-o-paper-airplane')
497
+                        ->visible(function (Invoice $record) {
498
+                            return $record->status === InvoiceStatus::Unsent;
499
+                        })
500
+                        ->action(function (Invoice $record) {
501
+                            $record->updateQuietly([
502
+                                'status' => InvoiceStatus::Sent,
503
+                            ]);
504
+                        }),
505
+                    Tables\Actions\Action::make('recordPayment')
506
+                        ->label('Record Payment')
507
+                        ->modalWidth(MaxWidth::TwoExtraLarge)
508
+                        ->icon('heroicon-o-credit-card')
509
+                        ->visible(function (Invoice $record) {
510
+                            return $record->canRecordPayment();
511
+                        })
512
+                        ->mountUsing(function (Invoice $record, Form $form) {
513
+                            $form->fill([
514
+                                'date' => now(),
515
+                                'amount' => $record->amount_due,
516
+                            ]);
517
+                        })
518
+                        ->form([
519
+                            Forms\Components\DatePicker::make('date')
520
+                                ->label('Payment Date'),
521
+                            Forms\Components\TextInput::make('amount')
522
+                                ->label('Amount')
523
+                                ->required()
524
+                                ->money(),
525
+                            Forms\Components\Select::make('payment_method')
526
+                                ->label('Payment Method')
527
+                                ->options([
528
+                                    'bank_payment' => 'Bank Payment',
529
+                                    'cash' => 'Cash',
530
+                                    'check' => 'Check',
531
+                                    'credit_card' => 'Credit Card',
532
+                                    'paypal' => 'PayPal',
533
+                                    'other' => 'Other',
534
+                                ]),
535
+                            Forms\Components\Select::make('bank_account_id')
536
+                                ->label('Account')
537
+                                ->options(BankAccount::query()
538
+                                    ->get()
539
+                                    ->pluck('account.name', 'id'))
540
+                                ->searchable(),
541
+                            Forms\Components\Textarea::make('notes')
542
+                                ->label('Notes'),
543
+                        ])
544
+                        ->action(function (Invoice $record, array $data) {
545
+                            $payment = $record->payments()->create([
546
+                                'date' => $data['date'],
547
+                                'amount' => $data['amount'],
548
+                                'payment_method' => $data['payment_method'],
549
+                                'bank_account_id' => $data['bank_account_id'],
550
+                                'notes' => $data['notes'],
551
+                            ]);
552
+
553
+                            $amountPaid = $record->getRawOriginal('amount_paid');
554
+                            $paymentAmount = $payment->getRawOriginal('amount');
555
+                            $totalAmount = $record->getRawOriginal('total');
556
+
557
+                            $newAmountPaid = $amountPaid + $paymentAmount;
558
+
559
+                            $record->amount_paid = CurrencyConverter::convertCentsToFloat($newAmountPaid);
560
+
561
+                            if ($newAmountPaid > $totalAmount) {
562
+                                $record->status = InvoiceStatus::Overpaid;
563
+                            } elseif ($newAmountPaid === $totalAmount) {
564
+                                $record->status = InvoiceStatus::Paid;
565
+                            } else {
566
+                                $record->status = InvoiceStatus::Partial;
567
+                            }
568
+
569
+                            $record->save();
570
+                        }),
571
+                ]),
444 572
             ])
445 573
             ->bulkActions([
446 574
                 Tables\Actions\BulkActionGroup::make([

+ 15
- 0
app/Models/Accounting/Invoice.php View File

@@ -71,6 +71,21 @@ class Invoice extends Model
71 71
         return $this->morphMany(Payment::class, 'payable');
72 72
     }
73 73
 
74
+    public function isDraft(): bool
75
+    {
76
+        return $this->status === InvoiceStatus::Draft;
77
+    }
78
+
79
+    public function canRecordPayment(): bool
80
+    {
81
+        return ! in_array($this->status, [
82
+            InvoiceStatus::Draft,
83
+            InvoiceStatus::Paid,
84
+            InvoiceStatus::Overpaid,
85
+            InvoiceStatus::Void,
86
+        ]);
87
+    }
88
+
74 89
     public static function getNextDocumentNumber(): string
75 90
     {
76 91
         $company = auth()->user()->currentCompany;

+ 9
- 4
app/Models/Banking/BankAccount.php View File

@@ -51,14 +51,14 @@ class BankAccount extends Model
51 51
         'mask',
52 52
     ];
53 53
 
54
-    public function connectedBankAccount(): HasOne
54
+    public function account(): BelongsTo
55 55
     {
56
-        return $this->hasOne(ConnectedBankAccount::class, 'bank_account_id');
56
+        return $this->belongsTo(Account::class, 'account_id');
57 57
     }
58 58
 
59
-    public function account(): BelongsTo
59
+    public function connectedBankAccount(): HasOne
60 60
     {
61
-        return $this->belongsTo(Account::class, 'account_id');
61
+        return $this->hasOne(ConnectedBankAccount::class, 'bank_account_id');
62 62
     }
63 63
 
64 64
     public function institution(): BelongsTo
@@ -66,6 +66,11 @@ class BankAccount extends Model
66 66
         return $this->belongsTo(Institution::class, 'institution_id');
67 67
     }
68 68
 
69
+    public function payments(): HasMany
70
+    {
71
+        return $this->hasMany(Payment::class, 'bank_account_id');
72
+    }
73
+
69 74
     public function transactions(): HasMany
70 75
     {
71 76
         return $this->hasMany(Transaction::class, 'bank_account_id');

+ 17
- 14
app/Providers/MacroServiceProvider.php View File

@@ -34,25 +34,28 @@ class MacroServiceProvider extends ServiceProvider
34 34
      */
35 35
     public function boot(): void
36 36
     {
37
-        TextInput::macro('money', function (string | Closure | null $currency = null): static {
37
+        TextInput::macro('money', function (string | Closure | null $currency = null, bool $useAffix = true): static {
38 38
             $currency ??= CurrencyAccessor::getDefaultCurrency();
39 39
 
40
-            $this
41
-                ->prefix(static function (TextInput $component) use ($currency) {
42
-                    $currency = $component->evaluate($currency);
40
+            if ($useAffix) {
41
+                $this
42
+                    ->prefix(static function (TextInput $component) use ($currency) {
43
+                        $currency = $component->evaluate($currency);
43 44
 
44
-                    return currency($currency)->getPrefix();
45
-                })
46
-                ->suffix(static function (TextInput $component) use ($currency) {
47
-                    $currency = $component->evaluate($currency);
45
+                        return currency($currency)->getPrefix();
46
+                    })
47
+                    ->suffix(static function (TextInput $component) use ($currency) {
48
+                        $currency = $component->evaluate($currency);
48 49
 
49
-                    return currency($currency)->getSuffix();
50
-                })
51
-                ->mask(static function (TextInput $component) use ($currency) {
52
-                    $currency = $component->evaluate($currency);
50
+                        return currency($currency)->getSuffix();
51
+                    });
52
+            }
53 53
 
54
-                    return moneyMask($currency);
55
-                });
54
+            $this->mask(static function (TextInput $component) use ($currency) {
55
+                $currency = $component->evaluate($currency);
56
+
57
+                return moneyMask($currency);
58
+            });
56 59
 
57 60
             return $this;
58 61
         });

+ 175
- 172
composer.lock
File diff suppressed because it is too large
View File


Loading…
Cancel
Save