Andrew Wallo 10 月之前
父節點
當前提交
4d140ff66b

+ 3
- 1
app/Enums/Accounting/InvoiceStatus.php 查看文件

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

+ 130
- 2
app/Filament/Company/Resources/Sales/InvoiceResource.php 查看文件

2
 
2
 
3
 namespace App\Filament\Company\Resources\Sales;
3
 namespace App\Filament\Company\Resources\Sales;
4
 
4
 
5
+use App\Enums\Accounting\InvoiceStatus;
5
 use App\Filament\Company\Resources\Sales\InvoiceResource\Pages;
6
 use App\Filament\Company\Resources\Sales\InvoiceResource\Pages;
6
 use App\Models\Accounting\Adjustment;
7
 use App\Models\Accounting\Adjustment;
7
 use App\Models\Accounting\DocumentLineItem;
8
 use App\Models\Accounting\DocumentLineItem;
8
 use App\Models\Accounting\Invoice;
9
 use App\Models\Accounting\Invoice;
10
+use App\Models\Banking\BankAccount;
9
 use App\Models\Common\Offering;
11
 use App\Models\Common\Offering;
10
 use App\Utilities\Currency\CurrencyAccessor;
12
 use App\Utilities\Currency\CurrencyAccessor;
13
+use App\Utilities\Currency\CurrencyConverter;
11
 use Awcodes\TableRepeater\Components\TableRepeater;
14
 use Awcodes\TableRepeater\Components\TableRepeater;
12
 use Awcodes\TableRepeater\Header;
15
 use Awcodes\TableRepeater\Header;
13
 use Carbon\CarbonInterface;
16
 use Carbon\CarbonInterface;
439
                 //
442
                 //
440
             ])
443
             ])
441
             ->actions([
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
             ->bulkActions([
573
             ->bulkActions([
446
                 Tables\Actions\BulkActionGroup::make([
574
                 Tables\Actions\BulkActionGroup::make([

+ 15
- 0
app/Models/Accounting/Invoice.php 查看文件

71
         return $this->morphMany(Payment::class, 'payable');
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
     public static function getNextDocumentNumber(): string
89
     public static function getNextDocumentNumber(): string
75
     {
90
     {
76
         $company = auth()->user()->currentCompany;
91
         $company = auth()->user()->currentCompany;

+ 9
- 4
app/Models/Banking/BankAccount.php 查看文件

51
         'mask',
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
     public function institution(): BelongsTo
64
     public function institution(): BelongsTo
66
         return $this->belongsTo(Institution::class, 'institution_id');
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
     public function transactions(): HasMany
74
     public function transactions(): HasMany
70
     {
75
     {
71
         return $this->hasMany(Transaction::class, 'bank_account_id');
76
         return $this->hasMany(Transaction::class, 'bank_account_id');

+ 17
- 14
app/Providers/MacroServiceProvider.php 查看文件

34
      */
34
      */
35
     public function boot(): void
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
             $currency ??= CurrencyAccessor::getDefaultCurrency();
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
             return $this;
60
             return $this;
58
         });
61
         });

+ 175
- 172
composer.lock
文件差異過大導致無法顯示
查看文件


Loading…
取消
儲存