Andrew Wallo 4 months ago
parent
commit
2e0fdeeeb2

+ 1
- 1
app/Filament/Company/Resources/Purchases/BillResource/Pages/ListBills.php View File

@@ -22,7 +22,7 @@ class ListBills extends ListRecords
22 22
         return [
23 23
             Actions\Action::make('payBills')
24 24
                 ->outlined()
25
-                ->url(BillResource::getUrl('pay-bills')),
25
+                ->url(PayBills::getUrl()),
26 26
             Actions\CreateAction::make(),
27 27
         ];
28 28
     }

+ 8
- 152
app/Filament/Company/Resources/Sales/InvoiceResource.php View File

@@ -8,7 +8,6 @@ use App\Enums\Accounting\AdjustmentType;
8 8
 use App\Enums\Accounting\DocumentDiscountMethod;
9 9
 use App\Enums\Accounting\DocumentType;
10 10
 use App\Enums\Accounting\InvoiceStatus;
11
-use App\Enums\Accounting\PaymentMethod;
12 11
 use App\Enums\Setting\PaymentTerms;
13 12
 use App\Filament\Company\Resources\Sales\ClientResource\RelationManagers\InvoicesRelationManager;
14 13
 use App\Filament\Company\Resources\Sales\InvoiceResource\Pages;
@@ -27,14 +26,12 @@ use App\Filament\Tables\Filters\DateRangeFilter;
27 26
 use App\Models\Accounting\Adjustment;
28 27
 use App\Models\Accounting\DocumentLineItem;
29 28
 use App\Models\Accounting\Invoice;
30
-use App\Models\Banking\BankAccount;
31 29
 use App\Models\Common\Client;
32 30
 use App\Models\Common\Offering;
33 31
 use App\Utilities\Currency\CurrencyAccessor;
34 32
 use App\Utilities\Currency\CurrencyConverter;
35 33
 use App\Utilities\RateCalculator;
36 34
 use Awcodes\TableRepeater\Header;
37
-use Closure;
38 35
 use Filament\Forms;
39 36
 use Filament\Forms\Form;
40 37
 use Filament\Notifications\Notification;
@@ -489,161 +486,20 @@ class InvoiceResource extends Resource
489 486
                         Invoice::getReplicateAction(Tables\Actions\ReplicateAction::class),
490 487
                         Invoice::getApproveDraftAction(Tables\Actions\Action::class),
491 488
                         Invoice::getMarkAsSentAction(Tables\Actions\Action::class),
492
-                        Tables\Actions\Action::make('recordPaymentBulk')
489
+                        Tables\Actions\Action::make('recordPayment')
493 490
                             ->label('Record Payment')
494 491
                             ->icon('heroicon-m-credit-card')
492
+                            ->visible(function (Invoice $record) {
493
+                                return $record->canRecordPayment();
494
+                            })
495 495
                             ->url(fn (Invoice $record) => Pages\RecordPayments::getUrl([
496
-                                'tableFilters' => ['client_id' => ['value' => $record->client_id]],
496
+                                'tableFilters' => [
497
+                                    'client_id' => ['value' => $record->client_id],
498
+                                    'currency_code' => ['value' => $record->currency_code],
499
+                                ],
497 500
                                 'invoice_id' => $record->id,
498 501
                             ]))
499 502
                             ->openUrlInNewTab(false),
500
-                        Tables\Actions\Action::make('recordPayment')
501
-                            ->label(fn (Invoice $record) => $record->status === InvoiceStatus::Overpaid ? 'Refund Overpayment' : 'Record Payment')
502
-                            ->slideOver()
503
-                            ->modalWidth(MaxWidth::TwoExtraLarge)
504
-                            ->icon('heroicon-m-credit-card')
505
-                            ->visible(function (Invoice $record) {
506
-                                return $record->canRecordPayment();
507
-                            })
508
-                            ->mountUsing(function (Invoice $record, Form $form) {
509
-                                $form->fill([
510
-                                    'posted_at' => now(),
511
-                                    'amount' => abs($record->amount_due),
512
-                                ]);
513
-                            })
514
-                            ->databaseTransaction()
515
-                            ->successNotificationTitle('Payment recorded')
516
-                            ->form([
517
-                                Forms\Components\DatePicker::make('posted_at')
518
-                                    ->label('Date'),
519
-                                Forms\Components\Grid::make()
520
-                                    ->schema([
521
-                                        Forms\Components\Select::make('bank_account_id')
522
-                                            ->label('Account')
523
-                                            ->required()
524
-                                            ->live()
525
-                                            ->options(function () {
526
-                                                return BankAccount::query()
527
-                                                    ->join('accounts', 'bank_accounts.account_id', '=', 'accounts.id')
528
-                                                    ->select(['bank_accounts.id', 'accounts.name', 'accounts.currency_code'])
529
-                                                    ->get()
530
-                                                    ->mapWithKeys(function ($account) {
531
-                                                        $label = $account->name;
532
-                                                        if ($account->currency_code) {
533
-                                                            $label .= " ({$account->currency_code})";
534
-                                                        }
535
-
536
-                                                        return [$account->id => $label];
537
-                                                    })
538
-                                                    ->toArray();
539
-                                            })
540
-                                            ->searchable(),
541
-                                        Forms\Components\TextInput::make('amount')
542
-                                            ->label('Amount')
543
-                                            ->required()
544
-                                            ->money(fn (Invoice $record) => $record->currency_code)
545
-                                            ->live(onBlur: true)
546
-                                            ->helperText(function (Invoice $record, $state) {
547
-                                                $invoiceCurrency = $record->currency_code;
548
-
549
-                                                if (! CurrencyConverter::isValidAmount($state, 'USD')) {
550
-                                                    return null;
551
-                                                }
552
-
553
-                                                $amountDue = $record->amount_due;
554
-
555
-                                                $amount = CurrencyConverter::convertToCents($state, 'USD');
556
-
557
-                                                if ($amount <= 0) {
558
-                                                    return 'Please enter a valid positive amount';
559
-                                                }
560
-
561
-                                                if ($record->status === InvoiceStatus::Overpaid) {
562
-                                                    $newAmountDue = $amountDue + $amount;
563
-                                                } else {
564
-                                                    $newAmountDue = $amountDue - $amount;
565
-                                                }
566
-
567
-                                                return match (true) {
568
-                                                    $newAmountDue > 0 => 'Amount due after payment will be ' . CurrencyConverter::formatCentsToMoney($newAmountDue, $invoiceCurrency),
569
-                                                    $newAmountDue === 0 => 'Invoice will be fully paid',
570
-                                                    default => 'Invoice will be overpaid by ' . CurrencyConverter::formatCentsToMoney(abs($newAmountDue), $invoiceCurrency),
571
-                                                };
572
-                                            })
573
-                                            ->rules([
574
-                                                static fn (): Closure => static function (string $attribute, $value, Closure $fail) {
575
-                                                    if (! CurrencyConverter::isValidAmount($value, 'USD')) {
576
-                                                        $fail('Please enter a valid amount');
577
-                                                    }
578
-                                                },
579
-                                            ]),
580
-                                    ])->columns(2),
581
-                                Forms\Components\Placeholder::make('currency_conversion')
582
-                                    ->label('Currency Conversion')
583
-                                    ->content(function (Forms\Get $get, Invoice $record) {
584
-                                        $amount = $get('amount');
585
-                                        $bankAccountId = $get('bank_account_id');
586
-
587
-                                        $invoiceCurrency = $record->currency_code;
588
-
589
-                                        if (empty($amount) || empty($bankAccountId) || ! CurrencyConverter::isValidAmount($amount, 'USD')) {
590
-                                            return null;
591
-                                        }
592
-
593
-                                        $bankAccount = BankAccount::with('account')->find($bankAccountId);
594
-                                        if (! $bankAccount) {
595
-                                            return null;
596
-                                        }
597
-
598
-                                        $bankCurrency = $bankAccount->account->currency_code ?? CurrencyAccessor::getDefaultCurrency();
599
-
600
-                                        // If currencies are the same, no conversion needed
601
-                                        if ($invoiceCurrency === $bankCurrency) {
602
-                                            return null;
603
-                                        }
604
-
605
-                                        // Convert amount from invoice currency to bank currency
606
-                                        $amountInInvoiceCurrencyCents = CurrencyConverter::convertToCents($amount, 'USD');
607
-                                        $amountInBankCurrencyCents = CurrencyConverter::convertBalance(
608
-                                            $amountInInvoiceCurrencyCents,
609
-                                            $invoiceCurrency,
610
-                                            $bankCurrency
611
-                                        );
612
-
613
-                                        $formattedBankAmount = CurrencyConverter::formatCentsToMoney($amountInBankCurrencyCents, $bankCurrency);
614
-
615
-                                        return "Payment will be recorded as {$formattedBankAmount} in the bank account's currency ({$bankCurrency}).";
616
-                                    })
617
-                                    ->hidden(function (Forms\Get $get, Invoice $record) {
618
-                                        $bankAccountId = $get('bank_account_id');
619
-                                        if (empty($bankAccountId)) {
620
-                                            return true;
621
-                                        }
622
-
623
-                                        $invoiceCurrency = $record->currency_code;
624
-
625
-                                        $bankAccount = BankAccount::with('account')->find($bankAccountId);
626
-                                        if (! $bankAccount) {
627
-                                            return true;
628
-                                        }
629
-
630
-                                        $bankCurrency = $bankAccount->account->currency_code ?? CurrencyAccessor::getDefaultCurrency();
631
-
632
-                                        // Hide if currencies are the same
633
-                                        return $invoiceCurrency === $bankCurrency;
634
-                                    }),
635
-                                Forms\Components\Select::make('payment_method')
636
-                                    ->label('Payment method')
637
-                                    ->required()
638
-                                    ->options(PaymentMethod::class),
639
-                                Forms\Components\Textarea::make('notes')
640
-                                    ->label('Notes'),
641
-                            ])
642
-                            ->action(function (Invoice $record, Tables\Actions\Action $action, array $data) {
643
-                                $record->recordPayment($data);
644
-
645
-                                $action->success();
646
-                            }),
647 503
                     ])->dropdown(false),
648 504
                     Tables\Actions\DeleteAction::make(),
649 505
                 ]),

+ 3
- 0
app/Filament/Company/Resources/Sales/InvoiceResource/Pages/ListInvoices.php View File

@@ -79,6 +79,9 @@ class ListInvoices extends ListRecords
79 79
     protected function getHeaderActions(): array
80 80
     {
81 81
         return [
82
+            Actions\Action::make('recordPayments')
83
+                ->outlined()
84
+                ->url(RecordPayments::getUrl()),
82 85
             Actions\CreateAction::make(),
83 86
         ];
84 87
     }

+ 110
- 12
app/Filament/Company/Resources/Sales/InvoiceResource/Pages/RecordPayments.php View File

@@ -18,6 +18,7 @@ use Filament\Forms;
18 18
 use Filament\Forms\Form;
19 19
 use Filament\Notifications\Notification;
20 20
 use Filament\Resources\Pages\ListRecords;
21
+use Filament\Support\Enums\MaxWidth;
21 22
 use Filament\Support\RawJs;
22 23
 use Filament\Tables;
23 24
 use Filament\Tables\Columns\Summarizers\Summarizer;
@@ -44,6 +45,9 @@ class RecordPayments extends ListRecords
44 45
 
45 46
     public ?array $data = [];
46 47
 
48
+    // New property for allocation amount
49
+    public ?int $allocationAmount = null;
50
+
47 51
     #[Url(as: 'invoice_id')]
48 52
     public ?int $invoiceId = null;
49 53
 
@@ -57,17 +61,21 @@ class RecordPayments extends ListRecords
57 61
         return 'Record Payments';
58 62
     }
59 63
 
64
+    public function getMaxContentWidth(): MaxWidth | string | null
65
+    {
66
+        return 'max-w-8xl';
67
+    }
68
+
60 69
     public function mount(): void
61 70
     {
62 71
         parent::mount();
63 72
 
64
-        $this->form->fill();
65
-
66 73
         $preservedClientId = $this->tableFilters['client_id']['value'] ?? null;
74
+        $preservedCurrencyCode = $this->tableFilters['currency_code']['value'] ?? CurrencyAccessor::getDefaultCurrency();
67 75
 
68 76
         $this->tableFilters = [
69 77
             'client_id' => $preservedClientId ? ['value' => $preservedClientId] : [],
70
-            'currency_code' => ['value' => CurrencyAccessor::getDefaultCurrency()],
78
+            'currency_code' => ['value' => $preservedCurrencyCode],
71 79
         ];
72 80
 
73 81
         // Auto-fill payment amount if invoice_id is provided
@@ -77,6 +85,8 @@ class RecordPayments extends ListRecords
77 85
                 $this->paymentAmounts[$invoiceId] = $invoice->amount_due;
78 86
             }
79 87
         }
88
+
89
+        $this->form->fill();
80 90
     }
81 91
 
82 92
     protected function getHeaderActions(): array
@@ -124,13 +134,37 @@ class RecordPayments extends ListRecords
124 134
                         ->success()
125 135
                         ->send();
126 136
 
127
-                    $this->reset('paymentAmounts');
137
+                    $this->reset('paymentAmounts', 'allocationAmount');
128 138
 
129 139
                     $this->resetTable();
130 140
                 }),
131 141
         ];
132 142
     }
133 143
 
144
+    protected function allocateOldestFirst(Collection $invoices, int $amountInCents): void
145
+    {
146
+        $remainingAmount = $amountInCents;
147
+
148
+        $sortedInvoices = $invoices->sortBy('due_date');
149
+
150
+        foreach ($sortedInvoices as $invoice) {
151
+            if ($remainingAmount <= 0) {
152
+                break;
153
+            }
154
+
155
+            $amountDue = $invoice->amount_due;
156
+            $allocation = min($remainingAmount, $amountDue);
157
+
158
+            $this->paymentAmounts[$invoice->id] = $allocation;
159
+            $remainingAmount -= $allocation;
160
+        }
161
+    }
162
+
163
+    protected function hasSelectedClient(): bool
164
+    {
165
+        return ! empty($this->getTableFilterState('client_id')['value']);
166
+    }
167
+
134 168
     /**
135 169
      * @return array<int | string, string | Form>
136 170
      */
@@ -146,7 +180,7 @@ class RecordPayments extends ListRecords
146 180
         return $form
147 181
             ->live()
148 182
             ->schema([
149
-                Forms\Components\Grid::make(3)
183
+                Forms\Components\Grid::make(2) // Changed from 3 to 4
150 184
                     ->schema([
151 185
                         Forms\Components\Select::make('bank_account_id')
152 186
                             ->label('Account')
@@ -167,6 +201,30 @@ class RecordPayments extends ListRecords
167 201
                             ->options(PaymentMethod::class)
168 202
                             ->default(PaymentMethod::BankPayment)
169 203
                             ->softRequired(),
204
+                        // Allocation amount field with suffix action
205
+                        Forms\Components\TextInput::make('allocation_amount')
206
+                            ->label('Allocate Payment Amount')
207
+                            ->live()
208
+                            ->default(array_sum($this->paymentAmounts))
209
+                            ->money($this->getTableFilterState('currency_code')['value'])
210
+                            ->suffixAction(
211
+                                Forms\Components\Actions\Action::make('allocate')
212
+                                    ->icon('heroicon-m-calculator')
213
+                                    ->action(function ($state) {
214
+                                        $this->allocationAmount = CurrencyConverter::convertToCents($state, 'USD');
215
+                                        if ($this->allocationAmount && $this->hasSelectedClient()) {
216
+                                            $this->allocateOldestFirst($this->getTableRecords(), $this->allocationAmount);
217
+
218
+                                            $amountFormatted = CurrencyConverter::formatCentsToMoney($this->allocationAmount, 'USD', true);
219
+
220
+                                            Notification::make()
221
+                                                ->title('Payment allocated')
222
+                                                ->body("Allocated {$amountFormatted} across invoices")
223
+                                                ->success()
224
+                                                ->send();
225
+                                        }
226
+                                    })
227
+                            ),
170 228
                     ]),
171 229
             ])->statePath('data');
172 230
     }
@@ -259,24 +317,46 @@ class RecordPayments extends ListRecords
259 317
                         Summarizer::make()
260 318
                             ->using(function () {
261 319
                                 $total = array_sum($this->paymentAmounts);
262
-                                $defaultCurrency = CurrencyAccessor::getDefaultCurrency();
263
-                                $activeCurrency = $this->getTableFilterState('currency_code')['value'] ?? $defaultCurrency;
320
+                                $bankAccountCurrency = $this->getSelectedBankAccount()->account->currency_code;
321
+                                $activeCurrency = $this->getTableFilterState('currency_code')['value'] ?? $bankAccountCurrency;
264 322
 
265
-                                if ($activeCurrency !== $defaultCurrency) {
266
-                                    $total = CurrencyConverter::convertBalance($total, $activeCurrency, $defaultCurrency);
323
+                                if ($activeCurrency !== $bankAccountCurrency) {
324
+                                    $total = CurrencyConverter::convertBalance($total, $activeCurrency, $bankAccountCurrency);
267 325
                                 }
268 326
 
269
-                                return CurrencyConverter::formatCentsToMoney($total, $defaultCurrency, true);
327
+                                return CurrencyConverter::formatCentsToMoney($total, $bankAccountCurrency, true);
270 328
                             }),
271 329
                         Summarizer::make()
272 330
                             ->using(fn () => $this->totalPaymentAmount)
273 331
                             ->visible(function () {
274 332
                                 $activeCurrency = $this->getTableFilterState('currency_code')['value'] ?? null;
275
-                                $defaultCurrency = CurrencyAccessor::getDefaultCurrency();
333
+                                $bankAccountCurrency = $this->getSelectedBankAccount()->account->currency_code;
276 334
 
277
-                                return $activeCurrency && $activeCurrency !== $defaultCurrency;
335
+                                return $activeCurrency && $activeCurrency !== $bankAccountCurrency;
278 336
                             }),
279 337
                     ]),
338
+                // New allocation status column
339
+                TextColumn::make('allocation_status')
340
+                    ->label('Status')
341
+                    ->getStateUsing(function (Invoice $record) {
342
+                        $paymentAmount = $this->paymentAmounts[$record->id] ?? 0;
343
+
344
+                        if ($paymentAmount <= 0) {
345
+                            return 'No payment';
346
+                        }
347
+
348
+                        if ($paymentAmount >= $record->amount_due) {
349
+                            return 'Full payment';
350
+                        }
351
+
352
+                        return 'Partial payment';
353
+                    })
354
+                    ->badge()
355
+                    ->color(fn (string $state): string => match ($state) {
356
+                        'Full payment' => 'success',
357
+                        'Partial payment' => 'warning',
358
+                        default => 'gray',
359
+                    }),
280 360
             ])
281 361
             ->bulkActions([
282 362
                 Tables\Actions\BulkAction::make('setFullAmounts')
@@ -360,6 +440,21 @@ class RecordPayments extends ListRecords
360 440
         return CurrencyConverter::formatCentsToMoney($total, $currencyCode, true);
361 441
     }
362 442
 
443
+    #[Computed]
444
+    public function allocationVariance(): string
445
+    {
446
+        if (! $this->allocationAmount) {
447
+            return '$0.00';
448
+        }
449
+
450
+        $totalAllocated = array_sum($this->paymentAmounts);
451
+        $variance = $this->allocationAmount - $totalAllocated;
452
+
453
+        $currencyCode = $this->getTableFilterState('currency_code')['value'];
454
+
455
+        return CurrencyConverter::formatCentsToMoney($variance, $currencyCode, true);
456
+    }
457
+
363 458
     public function getSelectedBankAccount(): BankAccount
364 459
     {
365 460
         $bankAccountId = $this->data['bank_account_id'];
@@ -377,5 +472,8 @@ class RecordPayments extends ListRecords
377 472
         $visibleInvoiceKeys = array_flip($visibleInvoiceIds);
378 473
 
379 474
         $this->paymentAmounts = array_intersect_key($this->paymentAmounts, $visibleInvoiceKeys);
475
+
476
+        // Reset allocation when client changes
477
+        $this->allocationAmount = null;
380 478
     }
381 479
 }

+ 1
- 1
resources/views/filament/company/resources/sales/invoice-resource/pages/record-payments.blade.php View File

@@ -6,7 +6,7 @@
6 6
 >
7 7
     <div class="flex flex-col gap-y-6">
8 8
         <x-filament::section>
9
-            <div class="flex items-center justify-between">
9
+            <div class="flex items-start justify-between">
10 10
                 <div>
11 11
                     {{ $this->form }}
12 12
                 </div>

Loading…
Cancel
Save