|
@@ -2,7 +2,6 @@
|
2
|
2
|
|
3
|
3
|
namespace App\Filament\Company\Resources\Sales;
|
4
|
4
|
|
5
|
|
-use App\Collections\Accounting\DocumentCollection;
|
6
|
5
|
use App\Enums\Accounting\AdjustmentCategory;
|
7
|
6
|
use App\Enums\Accounting\AdjustmentStatus;
|
8
|
7
|
use App\Enums\Accounting\AdjustmentType;
|
|
@@ -40,7 +39,6 @@ use Filament\Forms;
|
40
|
39
|
use Filament\Forms\Form;
|
41
|
40
|
use Filament\Notifications\Notification;
|
42
|
41
|
use Filament\Resources\Resource;
|
43
|
|
-use Filament\Support\Enums\Alignment;
|
44
|
42
|
use Filament\Support\Enums\MaxWidth;
|
45
|
43
|
use Filament\Tables;
|
46
|
44
|
use Filament\Tables\Table;
|
|
@@ -493,18 +491,16 @@ class InvoiceResource extends Resource
|
493
|
491
|
Invoice::getMarkAsSentAction(Tables\Actions\Action::class),
|
494
|
492
|
Tables\Actions\Action::make('recordPayment')
|
495
|
493
|
->label(fn (Invoice $record) => $record->status === InvoiceStatus::Overpaid ? 'Refund Overpayment' : 'Record Payment')
|
496
|
|
- ->stickyModalHeader()
|
497
|
|
- ->stickyModalFooter()
|
498
|
|
- ->modalFooterActionsAlignment(Alignment::End)
|
|
494
|
+ ->slideOver()
|
499
|
495
|
->modalWidth(MaxWidth::TwoExtraLarge)
|
500
|
|
- ->icon('heroicon-o-credit-card')
|
|
496
|
+ ->icon('heroicon-m-credit-card')
|
501
|
497
|
->visible(function (Invoice $record) {
|
502
|
498
|
return $record->canRecordPayment();
|
503
|
499
|
})
|
504
|
500
|
->mountUsing(function (Invoice $record, Form $form) {
|
505
|
501
|
$form->fill([
|
506
|
502
|
'posted_at' => now(),
|
507
|
|
- 'amount' => $record->status === InvoiceStatus::Overpaid ? ltrim($record->amount_due, '-') : $record->amount_due,
|
|
503
|
+ 'amount' => abs($record->amount_due),
|
508
|
504
|
]);
|
509
|
505
|
})
|
510
|
506
|
->databaseTransaction()
|
|
@@ -512,59 +508,126 @@ class InvoiceResource extends Resource
|
512
|
508
|
->form([
|
513
|
509
|
Forms\Components\DatePicker::make('posted_at')
|
514
|
510
|
->label('Date'),
|
515
|
|
- Forms\Components\TextInput::make('amount')
|
516
|
|
- ->label('Amount')
|
517
|
|
- ->required()
|
518
|
|
- ->money(fn (Invoice $record) => $record->currency_code)
|
519
|
|
- ->live(onBlur: true)
|
520
|
|
- ->helperText(function (Invoice $record, $state) {
|
|
511
|
+ Forms\Components\Grid::make()
|
|
512
|
+ ->schema([
|
|
513
|
+ Forms\Components\Select::make('bank_account_id')
|
|
514
|
+ ->label('Account')
|
|
515
|
+ ->required()
|
|
516
|
+ ->live()
|
|
517
|
+ ->options(function () {
|
|
518
|
+ return BankAccount::query()
|
|
519
|
+ ->join('accounts', 'bank_accounts.account_id', '=', 'accounts.id')
|
|
520
|
+ ->select(['bank_accounts.id', 'accounts.name', 'accounts.currency_code'])
|
|
521
|
+ ->get()
|
|
522
|
+ ->mapWithKeys(function ($account) {
|
|
523
|
+ $label = $account->name;
|
|
524
|
+ if ($account->currency_code) {
|
|
525
|
+ $label .= " ({$account->currency_code})";
|
|
526
|
+ }
|
|
527
|
+
|
|
528
|
+ return [$account->id => $label];
|
|
529
|
+ })
|
|
530
|
+ ->toArray();
|
|
531
|
+ })
|
|
532
|
+ ->searchable(),
|
|
533
|
+ Forms\Components\TextInput::make('amount')
|
|
534
|
+ ->label('Amount')
|
|
535
|
+ ->required()
|
|
536
|
+ ->money(fn (Invoice $record) => $record->currency_code)
|
|
537
|
+ ->live(onBlur: true)
|
|
538
|
+ ->helperText(function (Invoice $record, $state) {
|
|
539
|
+ $invoiceCurrency = $record->currency_code;
|
|
540
|
+
|
|
541
|
+ if (! CurrencyConverter::isValidAmount($state, 'USD')) {
|
|
542
|
+ return null;
|
|
543
|
+ }
|
|
544
|
+
|
|
545
|
+ $amountDue = $record->amount_due;
|
|
546
|
+
|
|
547
|
+ $amount = CurrencyConverter::convertToCents($state, 'USD');
|
|
548
|
+
|
|
549
|
+ if ($amount <= 0) {
|
|
550
|
+ return 'Please enter a valid positive amount';
|
|
551
|
+ }
|
|
552
|
+
|
|
553
|
+ if ($record->status === InvoiceStatus::Overpaid) {
|
|
554
|
+ $newAmountDue = $amountDue + $amount;
|
|
555
|
+ } else {
|
|
556
|
+ $newAmountDue = $amountDue - $amount;
|
|
557
|
+ }
|
|
558
|
+
|
|
559
|
+ return match (true) {
|
|
560
|
+ $newAmountDue > 0 => 'Amount due after payment will be ' . CurrencyConverter::formatCentsToMoney($newAmountDue, $invoiceCurrency),
|
|
561
|
+ $newAmountDue === 0 => 'Invoice will be fully paid',
|
|
562
|
+ default => 'Invoice will be overpaid by ' . CurrencyConverter::formatCentsToMoney(abs($newAmountDue), $invoiceCurrency),
|
|
563
|
+ };
|
|
564
|
+ })
|
|
565
|
+ ->rules([
|
|
566
|
+ static fn (): Closure => static function (string $attribute, $value, Closure $fail) {
|
|
567
|
+ if (! CurrencyConverter::isValidAmount($value, 'USD')) {
|
|
568
|
+ $fail('Please enter a valid amount');
|
|
569
|
+ }
|
|
570
|
+ },
|
|
571
|
+ ]),
|
|
572
|
+ ])->columns(2),
|
|
573
|
+ Forms\Components\Placeholder::make('currency_conversion')
|
|
574
|
+ ->label('Currency Conversion')
|
|
575
|
+ ->content(function (Forms\Get $get, Invoice $record) {
|
|
576
|
+ $amount = $get('amount');
|
|
577
|
+ $bankAccountId = $get('bank_account_id');
|
|
578
|
+
|
521
|
579
|
$invoiceCurrency = $record->currency_code;
|
522
|
|
- if (! CurrencyConverter::isValidAmount($state, 'USD')) {
|
|
580
|
+
|
|
581
|
+ if (empty($amount) || empty($bankAccountId) || ! CurrencyConverter::isValidAmount($amount, 'USD')) {
|
523
|
582
|
return null;
|
524
|
583
|
}
|
525
|
584
|
|
526
|
|
- $amountDue = $record->amount_due;
|
|
585
|
+ $bankAccount = BankAccount::with('account')->find($bankAccountId);
|
|
586
|
+ if (! $bankAccount) {
|
|
587
|
+ return null;
|
|
588
|
+ }
|
527
|
589
|
|
528
|
|
- $amount = CurrencyConverter::convertToCents($state, 'USD');
|
|
590
|
+ $bankCurrency = $bankAccount->account->currency_code ?? CurrencyAccessor::getDefaultCurrency();
|
529
|
591
|
|
530
|
|
- if ($amount <= 0) {
|
531
|
|
- return 'Please enter a valid positive amount';
|
|
592
|
+ // If currencies are the same, no conversion needed
|
|
593
|
+ if ($invoiceCurrency === $bankCurrency) {
|
|
594
|
+ return null;
|
532
|
595
|
}
|
533
|
596
|
|
534
|
|
- if ($record->status === InvoiceStatus::Overpaid) {
|
535
|
|
- $newAmountDue = $amountDue + $amount;
|
536
|
|
- } else {
|
537
|
|
- $newAmountDue = $amountDue - $amount;
|
538
|
|
- }
|
|
597
|
+ // Convert amount from invoice currency to bank currency
|
|
598
|
+ $amountInInvoiceCurrencyCents = CurrencyConverter::convertToCents($amount, 'USD');
|
|
599
|
+ $amountInBankCurrencyCents = CurrencyConverter::convertBalance(
|
|
600
|
+ $amountInInvoiceCurrencyCents,
|
|
601
|
+ $invoiceCurrency,
|
|
602
|
+ $bankCurrency
|
|
603
|
+ );
|
|
604
|
+
|
|
605
|
+ $formattedBankAmount = CurrencyConverter::formatCentsToMoney($amountInBankCurrencyCents, $bankCurrency);
|
539
|
606
|
|
540
|
|
- return match (true) {
|
541
|
|
- $newAmountDue > 0 => 'Amount due after payment will be ' . CurrencyConverter::formatCentsToMoney($newAmountDue, $invoiceCurrency),
|
542
|
|
- $newAmountDue === 0 => 'Invoice will be fully paid',
|
543
|
|
- default => 'Invoice will be overpaid by ' . CurrencyConverter::formatCentsToMoney(abs($newAmountDue), $invoiceCurrency),
|
544
|
|
- };
|
|
607
|
+ return "Payment will be recorded as {$formattedBankAmount} in the bank account's currency ({$bankCurrency}).";
|
545
|
608
|
})
|
546
|
|
- ->rules([
|
547
|
|
- static fn (): Closure => static function (string $attribute, $value, Closure $fail) {
|
548
|
|
- if (! CurrencyConverter::isValidAmount($value, 'USD')) {
|
549
|
|
- $fail('Please enter a valid amount');
|
550
|
|
- }
|
551
|
|
- },
|
552
|
|
- ]),
|
|
609
|
+ ->hidden(function (Forms\Get $get, Invoice $record) {
|
|
610
|
+ $bankAccountId = $get('bank_account_id');
|
|
611
|
+ if (empty($bankAccountId)) {
|
|
612
|
+ return true;
|
|
613
|
+ }
|
|
614
|
+
|
|
615
|
+ $invoiceCurrency = $record->currency_code;
|
|
616
|
+
|
|
617
|
+ $bankAccount = BankAccount::with('account')->find($bankAccountId);
|
|
618
|
+ if (! $bankAccount) {
|
|
619
|
+ return true;
|
|
620
|
+ }
|
|
621
|
+
|
|
622
|
+ $bankCurrency = $bankAccount->account->currency_code ?? CurrencyAccessor::getDefaultCurrency();
|
|
623
|
+
|
|
624
|
+ // Hide if currencies are the same
|
|
625
|
+ return $invoiceCurrency === $bankCurrency;
|
|
626
|
+ }),
|
553
|
627
|
Forms\Components\Select::make('payment_method')
|
554
|
628
|
->label('Payment method')
|
555
|
629
|
->required()
|
556
|
630
|
->options(PaymentMethod::class),
|
557
|
|
- Forms\Components\Select::make('bank_account_id')
|
558
|
|
- ->label('Account')
|
559
|
|
- ->required()
|
560
|
|
- ->options(function () {
|
561
|
|
- return BankAccount::query()
|
562
|
|
- ->join('accounts', 'bank_accounts.account_id', '=', 'accounts.id')
|
563
|
|
- ->select(['bank_accounts.id', 'accounts.name'])
|
564
|
|
- ->pluck('accounts.name', 'bank_accounts.id')
|
565
|
|
- ->toArray();
|
566
|
|
- })
|
567
|
|
- ->searchable(),
|
568
|
631
|
Forms\Components\Textarea::make('notes')
|
569
|
632
|
->label('Notes'),
|
570
|
633
|
])
|
|
@@ -670,111 +733,6 @@ class InvoiceResource extends Resource
|
670
|
733
|
$record->markAsSent();
|
671
|
734
|
});
|
672
|
735
|
|
673
|
|
- $action->success();
|
674
|
|
- }),
|
675
|
|
- Tables\Actions\BulkAction::make('recordPayments')
|
676
|
|
- ->label('Record payments')
|
677
|
|
- ->icon('heroicon-o-credit-card')
|
678
|
|
- ->stickyModalHeader()
|
679
|
|
- ->stickyModalFooter()
|
680
|
|
- ->modalFooterActionsAlignment(Alignment::End)
|
681
|
|
- ->modalWidth(MaxWidth::TwoExtraLarge)
|
682
|
|
- ->databaseTransaction()
|
683
|
|
- ->successNotificationTitle('Payments recorded')
|
684
|
|
- ->failureNotificationTitle('Failed to Record Payments')
|
685
|
|
- ->deselectRecordsAfterCompletion()
|
686
|
|
- ->beforeFormFilled(function (Collection $records, Tables\Actions\BulkAction $action) {
|
687
|
|
- $isInvalid = $records->contains(fn (Invoice $record) => ! $record->canBulkRecordPayment());
|
688
|
|
-
|
689
|
|
- if ($isInvalid) {
|
690
|
|
- Notification::make()
|
691
|
|
- ->title('Payment recording failed')
|
692
|
|
- ->body('Invoices that are either draft, paid, overpaid, voided, or are in a foreign currency cannot be processed through bulk payments. Please adjust your selection and try again.')
|
693
|
|
- ->persistent()
|
694
|
|
- ->danger()
|
695
|
|
- ->send();
|
696
|
|
-
|
697
|
|
- $action->cancel(true);
|
698
|
|
- }
|
699
|
|
- })
|
700
|
|
- ->mountUsing(function (DocumentCollection $records, Form $form) {
|
701
|
|
- $totalAmountDue = $records->sum('amount_due');
|
702
|
|
-
|
703
|
|
- $form->fill([
|
704
|
|
- 'posted_at' => now(),
|
705
|
|
- 'amount' => $totalAmountDue,
|
706
|
|
- ]);
|
707
|
|
- })
|
708
|
|
- ->form([
|
709
|
|
- Forms\Components\DatePicker::make('posted_at')
|
710
|
|
- ->label('Date'),
|
711
|
|
- Forms\Components\TextInput::make('amount')
|
712
|
|
- ->label('Amount')
|
713
|
|
- ->required()
|
714
|
|
- ->money()
|
715
|
|
- ->rules([
|
716
|
|
- static fn (): Closure => static function (string $attribute, $value, Closure $fail) {
|
717
|
|
- if (! CurrencyConverter::isValidAmount($value)) {
|
718
|
|
- $fail('Please enter a valid amount');
|
719
|
|
- }
|
720
|
|
- },
|
721
|
|
- ]),
|
722
|
|
- Forms\Components\Select::make('payment_method')
|
723
|
|
- ->label('Payment method')
|
724
|
|
- ->required()
|
725
|
|
- ->options(PaymentMethod::class),
|
726
|
|
- Forms\Components\Select::make('bank_account_id')
|
727
|
|
- ->label('Account')
|
728
|
|
- ->required()
|
729
|
|
- ->options(function () {
|
730
|
|
- return BankAccount::query()
|
731
|
|
- ->join('accounts', 'bank_accounts.account_id', '=', 'accounts.id')
|
732
|
|
- ->select(['bank_accounts.id', 'accounts.name'])
|
733
|
|
- ->pluck('accounts.name', 'bank_accounts.id')
|
734
|
|
- ->toArray();
|
735
|
|
- })
|
736
|
|
- ->searchable(),
|
737
|
|
- Forms\Components\Textarea::make('notes')
|
738
|
|
- ->label('Notes'),
|
739
|
|
- ])
|
740
|
|
- ->before(function (DocumentCollection $records, Tables\Actions\BulkAction $action, array $data) {
|
741
|
|
- $totalPaymentAmount = $data['amount'] ?? 0;
|
742
|
|
- $totalAmountDue = $records->sum('amount_due');
|
743
|
|
-
|
744
|
|
- if ($totalPaymentAmount > $totalAmountDue) {
|
745
|
|
- $formattedTotalAmountDue = CurrencyConverter::formatCentsToMoney($totalAmountDue);
|
746
|
|
-
|
747
|
|
- Notification::make()
|
748
|
|
- ->title('Excess payment amount')
|
749
|
|
- ->body("The payment amount exceeds the total amount due of {$formattedTotalAmountDue}. Please adjust the payment amount and try again.")
|
750
|
|
- ->persistent()
|
751
|
|
- ->warning()
|
752
|
|
- ->send();
|
753
|
|
-
|
754
|
|
- $action->halt(true);
|
755
|
|
- }
|
756
|
|
- })
|
757
|
|
- ->action(function (DocumentCollection $records, Tables\Actions\BulkAction $action, array $data) {
|
758
|
|
- $totalPaymentAmount = $data['amount'] ?? 0;
|
759
|
|
-
|
760
|
|
- $remainingAmount = $totalPaymentAmount;
|
761
|
|
-
|
762
|
|
- $records->each(function (Invoice $record) use (&$remainingAmount, $data) {
|
763
|
|
- $amountDue = $record->amount_due;
|
764
|
|
-
|
765
|
|
- if ($amountDue <= 0 || $remainingAmount <= 0) {
|
766
|
|
- return;
|
767
|
|
- }
|
768
|
|
-
|
769
|
|
- $paymentAmount = min($amountDue, $remainingAmount);
|
770
|
|
-
|
771
|
|
- $data['amount'] = $paymentAmount;
|
772
|
|
-
|
773
|
|
- $record->recordPayment($data);
|
774
|
|
-
|
775
|
|
- $remainingAmount -= $paymentAmount;
|
776
|
|
- });
|
777
|
|
-
|
778
|
736
|
$action->success();
|
779
|
737
|
}),
|
780
|
738
|
]),
|