Browse Source

Merge pull request #88 from andrewdwallo/development-3.x

Development 3.x
3.x
Andrew Wallo 10 months ago
parent
commit
54fee607b6
No account linked to committer's email address
36 changed files with 1125 additions and 731 deletions
  1. 16
    28
      app/Casts/RateCast.php
  2. 55
    0
      app/Collections/Accounting/DocumentCollection.php
  3. 0
    36
      app/Collections/Accounting/InvoiceCollection.php
  4. 15
    8
      app/Concerns/ManagesLineItems.php
  5. 45
    0
      app/Enums/Accounting/DocumentType.php
  6. 1
    1
      app/Filament/Company/Pages/Accounting/Transactions.php
  7. 1
    2
      app/Filament/Company/Resources/Banking/AccountResource.php
  8. 67
    31
      app/Filament/Company/Resources/Purchases/BillResource.php
  9. 7
    4
      app/Filament/Company/Resources/Purchases/BillResource/Widgets/BillOverview.php
  10. 0
    1
      app/Filament/Company/Resources/Purchases/VendorResource.php
  11. 1
    2
      app/Filament/Company/Resources/Sales/ClientResource.php
  12. 67
    35
      app/Filament/Company/Resources/Sales/InvoiceResource.php
  13. 1
    1
      app/Filament/Company/Resources/Sales/InvoiceResource/Pages/ViewInvoice.php
  14. 6
    4
      app/Filament/Company/Resources/Sales/InvoiceResource/Widgets/InvoiceOverview.php
  15. 0
    37
      app/Filament/Forms/Components/BillTotals.php
  16. 15
    10
      app/Filament/Forms/Components/CreateCurrencySelect.php
  17. 57
    0
      app/Filament/Forms/Components/DocumentTotals.php
  18. 0
    37
      app/Filament/Forms/Components/InvoiceTotals.php
  19. 29
    35
      app/Helpers/format.php
  20. 71
    13
      app/Models/Accounting/Bill.php
  21. 44
    0
      app/Models/Accounting/DocumentLineItem.php
  22. 67
    13
      app/Models/Accounting/Invoice.php
  23. 38
    15
      app/Observers/TransactionObserver.php
  24. 130
    34
      app/Providers/MacroServiceProvider.php
  25. 53
    0
      app/Utilities/RateCalculator.php
  26. 0
    78
      app/View/Models/BillTotalViewModel.php
  27. 126
    0
      app/View/Models/DocumentTotalViewModel.php
  28. 0
    78
      app/View/Models/InvoiceTotalViewModel.php
  29. 1
    1
      composer.json
  30. 85
    85
      composer.lock
  31. 1
    1
      config/money.php
  32. 12
    12
      package-lock.json
  33. 7
    7
      resources/views/filament/company/resources/sales/invoices/components/invoice-view.blade.php
  34. 0
    61
      resources/views/filament/forms/components/bill-totals.blade.php
  35. 107
    0
      resources/views/filament/forms/components/document-totals.blade.php
  36. 0
    61
      resources/views/filament/forms/components/invoice-totals.blade.php

+ 16
- 28
app/Casts/RateCast.php View File

@@ -2,65 +2,53 @@
2 2
 
3 3
 namespace App\Casts;
4 4
 
5
-use App\Enums\Setting\NumberFormat;
6
-use App\Models\Setting\Localization;
5
+use App\Enums\Accounting\AdjustmentComputation;
7 6
 use App\Utilities\Currency\CurrencyAccessor;
7
+use App\Utilities\RateCalculator;
8 8
 use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
9 9
 use Illuminate\Database\Eloquent\Model;
10 10
 
11 11
 class RateCast implements CastsAttributes
12 12
 {
13
-    private const PRECISION = 4;
14
-
15 13
     public function get($model, string $key, $value, array $attributes): string
16 14
     {
15
+        if (! $value) {
16
+            return '0';
17
+        }
18
+
17 19
         $currency_code = $this->getDefaultCurrencyCode();
18
-        $computation = $attributes['computation'] ?? null;
20
+        $computation = AdjustmentComputation::parse($attributes['computation'] ?? $attributes['discount_computation'] ?? null);
19 21
 
20
-        if ($computation === 'fixed') {
22
+        if ($computation?->isFixed()) {
21 23
             return money($value, $currency_code)->formatSimple();
22 24
         }
23 25
 
24
-        $floatValue = $value / (10 ** self::PRECISION);
25
-
26
-        $format = Localization::firstOrFail()->number_format->value;
27
-        [$decimal_mark, $thousands_separator] = NumberFormat::from($format)->getFormattingParameters();
28
-
29
-        return $this->formatWithoutTrailingZeros($floatValue, $decimal_mark, $thousands_separator);
26
+        return RateCalculator::formatScaledRate($value);
30 27
     }
31 28
 
32 29
     public function set(Model $model, string $key, mixed $value, array $attributes): int
33 30
     {
31
+        if (! $value) {
32
+            return 0;
33
+        }
34
+
34 35
         if (is_int($value)) {
35 36
             return $value;
36 37
         }
37 38
 
38
-        $computation = $attributes['computation'] ?? null;
39
+        $computation = AdjustmentComputation::parse($attributes['computation'] ?? $attributes['discount_computation'] ?? null);
39 40
 
40 41
         $currency_code = $this->getDefaultCurrencyCode();
41 42
 
42
-        if ($computation === 'fixed') {
43
+        if ($computation?->isFixed()) {
43 44
             return money($value, $currency_code, true)->getAmount();
44 45
         }
45 46
 
46
-        $format = Localization::firstOrFail()->number_format->value;
47
-        [$decimal_mark, $thousands_separator] = NumberFormat::from($format)->getFormattingParameters();
48
-
49
-        $intValue = str_replace([$thousands_separator, $decimal_mark], ['', '.'], $value);
50
-
51
-        return (int) round((float) $intValue * (10 ** self::PRECISION));
47
+        return RateCalculator::parseLocalizedRate($value);
52 48
     }
53 49
 
54 50
     private function getDefaultCurrencyCode(): string
55 51
     {
56 52
         return CurrencyAccessor::getDefaultCurrency();
57 53
     }
58
-
59
-    private function formatWithoutTrailingZeros($floatValue, $decimal_mark, $thousands_separator): string
60
-    {
61
-        $formatted = number_format($floatValue, self::PRECISION, $decimal_mark, $thousands_separator);
62
-        $formatted = rtrim($formatted, '0');
63
-
64
-        return rtrim($formatted, $decimal_mark);
65
-    }
66 54
 }

+ 55
- 0
app/Collections/Accounting/DocumentCollection.php View File

@@ -0,0 +1,55 @@
1
+<?php
2
+
3
+namespace App\Collections\Accounting;
4
+
5
+use App\Utilities\Currency\CurrencyAccessor;
6
+use App\Utilities\Currency\CurrencyConverter;
7
+use Illuminate\Database\Eloquent\Collection;
8
+
9
+class DocumentCollection extends Collection
10
+{
11
+    public function sumMoneyInCents(string $column): int
12
+    {
13
+        return $this->reduce(static function ($carry, $document) use ($column) {
14
+            return $carry + $document->getRawOriginal($column);
15
+        }, 0);
16
+    }
17
+
18
+    public function sumMoneyFormattedSimple(string $column, ?string $currency = null): string
19
+    {
20
+        $currency ??= CurrencyAccessor::getDefaultCurrency();
21
+
22
+        $totalCents = $this->sumMoneyInCents($column);
23
+
24
+        return CurrencyConverter::convertCentsToFormatSimple($totalCents, $currency);
25
+    }
26
+
27
+    public function sumMoneyFormatted(string $column, ?string $currency = null): string
28
+    {
29
+        $currency ??= CurrencyAccessor::getDefaultCurrency();
30
+
31
+        $totalCents = $this->sumMoneyInCents($column);
32
+
33
+        return CurrencyConverter::formatCentsToMoney($totalCents, $currency);
34
+    }
35
+
36
+    public function sumMoneyInDefaultCurrency(string $column): int
37
+    {
38
+        $defaultCurrency = CurrencyAccessor::getDefaultCurrency();
39
+
40
+        return $this->reduce(static function ($carry, $document) use ($column, $defaultCurrency) {
41
+            $amountInCents = $document->getRawOriginal($column);
42
+            $documentCurrency = $document->currency_code ?? $defaultCurrency;
43
+
44
+            if ($documentCurrency !== $defaultCurrency) {
45
+                $amountInCents = CurrencyConverter::convertBalance(
46
+                    $amountInCents,
47
+                    $documentCurrency,
48
+                    $defaultCurrency
49
+                );
50
+            }
51
+
52
+            return $carry + $amountInCents;
53
+        }, 0);
54
+    }
55
+}

+ 0
- 36
app/Collections/Accounting/InvoiceCollection.php View File

@@ -1,36 +0,0 @@
1
-<?php
2
-
3
-namespace App\Collections\Accounting;
4
-
5
-use App\Models\Accounting\Invoice;
6
-use App\Utilities\Currency\CurrencyAccessor;
7
-use App\Utilities\Currency\CurrencyConverter;
8
-use Illuminate\Database\Eloquent\Collection;
9
-
10
-class InvoiceCollection extends Collection
11
-{
12
-    public function sumMoneyInCents(string $column): int
13
-    {
14
-        return $this->reduce(static function ($carry, Invoice $invoice) use ($column) {
15
-            return $carry + $invoice->getRawOriginal($column);
16
-        }, 0);
17
-    }
18
-
19
-    public function sumMoneyFormattedSimple(string $column, ?string $currency = null): string
20
-    {
21
-        $currency ??= CurrencyAccessor::getDefaultCurrency();
22
-
23
-        $totalCents = $this->sumMoneyInCents($column);
24
-
25
-        return CurrencyConverter::convertCentsToFormatSimple($totalCents, $currency);
26
-    }
27
-
28
-    public function sumMoneyFormatted(string $column, ?string $currency = null): string
29
-    {
30
-        $currency ??= CurrencyAccessor::getDefaultCurrency();
31
-
32
-        $totalCents = $this->sumMoneyInCents($column);
33
-
34
-        return CurrencyConverter::formatCentsToMoney($totalCents, $currency);
35
-    }
36
-}

+ 15
- 8
app/Concerns/ManagesLineItems.php View File

@@ -6,7 +6,9 @@ use App\Enums\Accounting\AdjustmentComputation;
6 6
 use App\Enums\Accounting\DocumentDiscountMethod;
7 7
 use App\Models\Accounting\Bill;
8 8
 use App\Models\Accounting\DocumentLineItem;
9
+use App\Utilities\Currency\CurrencyAccessor;
9 10
 use App\Utilities\Currency\CurrencyConverter;
11
+use App\Utilities\RateCalculator;
10 12
 use Illuminate\Database\Eloquent\Model;
11 13
 use Illuminate\Support\Collection;
12 14
 
@@ -79,6 +81,7 @@ trait ManagesLineItems
79 81
 
80 82
     protected function updateDocumentTotals(Model $record, array $data): array
81 83
     {
84
+        $currencyCode = $data['currency_code'] ?? CurrencyAccessor::getDefaultCurrency();
82 85
         $subtotalCents = $record->lineItems()->sum('subtotal');
83 86
         $taxTotalCents = $record->lineItems()->sum('tax_total');
84 87
         $discountTotalCents = $this->calculateDiscountTotal(
@@ -86,16 +89,17 @@ trait ManagesLineItems
86 89
             AdjustmentComputation::parse($data['discount_computation']),
87 90
             $data['discount_rate'] ?? null,
88 91
             $subtotalCents,
89
-            $record
92
+            $record,
93
+            $currencyCode,
90 94
         );
91 95
 
92 96
         $grandTotalCents = $subtotalCents + $taxTotalCents - $discountTotalCents;
93 97
 
94 98
         return [
95
-            'subtotal' => CurrencyConverter::convertCentsToFormatSimple($subtotalCents),
96
-            'tax_total' => CurrencyConverter::convertCentsToFormatSimple($taxTotalCents),
97
-            'discount_total' => CurrencyConverter::convertCentsToFormatSimple($discountTotalCents),
98
-            'total' => CurrencyConverter::convertCentsToFormatSimple($grandTotalCents),
99
+            'subtotal' => CurrencyConverter::convertCentsToFormatSimple($subtotalCents, $currencyCode),
100
+            'tax_total' => CurrencyConverter::convertCentsToFormatSimple($taxTotalCents, $currencyCode),
101
+            'discount_total' => CurrencyConverter::convertCentsToFormatSimple($discountTotalCents, $currencyCode),
102
+            'total' => CurrencyConverter::convertCentsToFormatSimple($grandTotalCents, $currencyCode),
99 103
         ];
100 104
     }
101 105
 
@@ -104,16 +108,19 @@ trait ManagesLineItems
104 108
         ?AdjustmentComputation $discountComputation,
105 109
         ?string $discountRate,
106 110
         int $subtotalCents,
107
-        Model $record
111
+        Model $record,
112
+        string $currencyCode
108 113
     ): int {
109 114
         if ($discountMethod->isPerLineItem()) {
110 115
             return $record->lineItems()->sum('discount_total');
111 116
         }
112 117
 
113 118
         if ($discountComputation?->isPercentage()) {
114
-            return (int) ($subtotalCents * ((float) $discountRate / 100));
119
+            $scaledRate = RateCalculator::parseLocalizedRate($discountRate);
120
+
121
+            return RateCalculator::calculatePercentage($subtotalCents, $scaledRate);
115 122
         }
116 123
 
117
-        return CurrencyConverter::convertToCents($discountRate);
124
+        return CurrencyConverter::convertToCents($discountRate, $currencyCode);
118 125
     }
119 126
 }

+ 45
- 0
app/Enums/Accounting/DocumentType.php View File

@@ -0,0 +1,45 @@
1
+<?php
2
+
3
+namespace App\Enums\Accounting;
4
+
5
+use Filament\Support\Contracts\HasIcon;
6
+use Filament\Support\Contracts\HasLabel;
7
+
8
+enum DocumentType: string implements HasIcon, HasLabel
9
+{
10
+    case Invoice = 'invoice';
11
+    case Bill = 'bill';
12
+    // TODO: Add estimate
13
+    // case Estimate = 'estimate';
14
+
15
+    public const DEFAULT = self::Invoice->value;
16
+
17
+    public function getLabel(): ?string
18
+    {
19
+        return $this->name;
20
+    }
21
+
22
+    public function getIcon(): ?string
23
+    {
24
+        return match ($this->value) {
25
+            self::Invoice->value => 'heroicon-o-document-duplicate',
26
+            self::Bill->value => 'heroicon-o-clipboard-document-list',
27
+        };
28
+    }
29
+
30
+    public function getTaxKey(): string
31
+    {
32
+        return match ($this) {
33
+            self::Invoice => 'salesTaxes',
34
+            self::Bill => 'purchaseTaxes',
35
+        };
36
+    }
37
+
38
+    public function getDiscountKey(): string
39
+    {
40
+        return match ($this) {
41
+            self::Invoice => 'salesDiscounts',
42
+            self::Bill => 'purchaseDiscounts',
43
+        };
44
+    }
45
+}

+ 1
- 1
app/Filament/Company/Pages/Accounting/Transactions.php View File

@@ -305,7 +305,7 @@ class Transactions extends Page implements HasTable
305 305
                         }
306 306
                     )
307 307
                     ->sortable()
308
-                    ->currency(static fn (Transaction $transaction) => $transaction->bankAccount?->account->currency_code ?? CurrencyAccessor::getDefaultCurrency(), true),
308
+                    ->currency(static fn (Transaction $transaction) => $transaction->bankAccount?->account->currency_code),
309 309
             ])
310 310
             ->recordClasses(static fn (Transaction $transaction) => $transaction->reviewed ? 'bg-primary-300/10' : null)
311 311
             ->defaultSort('posted_at', 'desc')

+ 1
- 2
app/Filament/Company/Resources/Banking/AccountResource.php View File

@@ -82,8 +82,7 @@ class AccountResource extends Resource
82 82
                                     ->localizeLabel()
83 83
                                     ->required(),
84 84
                                 CreateCurrencySelect::make('currency_code')
85
-                                    ->disabledOn('edit')
86
-                                    ->relationship('currency', 'name'),
85
+                                    ->disabledOn('edit'),
87 86
                             ]),
88 87
                         Forms\Components\Group::make()
89 88
                             ->columns()

+ 67
- 31
app/Filament/Company/Resources/Purchases/BillResource.php View File

@@ -4,16 +4,21 @@ namespace App\Filament\Company\Resources\Purchases;
4 4
 
5 5
 use App\Enums\Accounting\BillStatus;
6 6
 use App\Enums\Accounting\DocumentDiscountMethod;
7
+use App\Enums\Accounting\DocumentType;
7 8
 use App\Enums\Accounting\PaymentMethod;
8 9
 use App\Filament\Company\Resources\Purchases\BillResource\Pages;
9
-use App\Filament\Forms\Components\BillTotals;
10
+use App\Filament\Forms\Components\CreateCurrencySelect;
11
+use App\Filament\Forms\Components\DocumentTotals;
10 12
 use App\Filament\Tables\Actions\ReplicateBulkAction;
11 13
 use App\Filament\Tables\Filters\DateRangeFilter;
12 14
 use App\Models\Accounting\Adjustment;
13 15
 use App\Models\Accounting\Bill;
14 16
 use App\Models\Banking\BankAccount;
15 17
 use App\Models\Common\Offering;
18
+use App\Models\Common\Vendor;
19
+use App\Utilities\Currency\CurrencyAccessor;
16 20
 use App\Utilities\Currency\CurrencyConverter;
21
+use App\Utilities\RateCalculator;
17 22
 use Awcodes\TableRepeater\Components\TableRepeater;
18 23
 use Awcodes\TableRepeater\Header;
19 24
 use Closure;
@@ -49,7 +54,20 @@ class BillResource extends Resource
49 54
                                     ->relationship('vendor', 'name')
50 55
                                     ->preload()
51 56
                                     ->searchable()
52
-                                    ->required(),
57
+                                    ->required()
58
+                                    ->live()
59
+                                    ->afterStateUpdated(function (Forms\Set $set, Forms\Get $get, $state) {
60
+                                        if (! $state) {
61
+                                            return;
62
+                                        }
63
+
64
+                                        $currencyCode = Vendor::find($state)?->currency_code;
65
+
66
+                                        if ($currencyCode) {
67
+                                            $set('currency_code', $currencyCode);
68
+                                        }
69
+                                    }),
70
+                                CreateCurrencySelect::make('currency_code'),
53 71
                             ]),
54 72
                             Forms\Components\Group::make([
55 73
                                 Forms\Components\TextInput::make('bill_number')
@@ -111,6 +129,7 @@ class BillResource extends Resource
111 129
                             })
112 130
                             ->schema([
113 131
                                 Forms\Components\Select::make('offering_id')
132
+                                    ->label('Item')
114 133
                                     ->relationship('purchasableOffering', 'name')
115 134
                                     ->preload()
116 135
                                     ->searchable()
@@ -138,11 +157,13 @@ class BillResource extends Resource
138 157
                                     ->live()
139 158
                                     ->default(1),
140 159
                                 Forms\Components\TextInput::make('unit_price')
160
+                                    ->label('Price')
141 161
                                     ->hiddenLabel()
142 162
                                     ->numeric()
143 163
                                     ->live()
144 164
                                     ->default(0),
145 165
                                 Forms\Components\Select::make('purchaseTaxes')
166
+                                    ->label('Taxes')
146 167
                                     ->relationship('purchaseTaxes', 'name')
147 168
                                     ->saveRelationshipsUsing(null)
148 169
                                     ->dehydrated(true)
@@ -151,6 +172,7 @@ class BillResource extends Resource
151 172
                                     ->live()
152 173
                                     ->searchable(),
153 174
                                 Forms\Components\Select::make('purchaseDiscounts')
175
+                                    ->label('Discounts')
154 176
                                     ->relationship('purchaseDiscounts', 'name')
155 177
                                     ->saveRelationshipsUsing(null)
156 178
                                     ->dehydrated(true)
@@ -165,35 +187,46 @@ class BillResource extends Resource
165 187
                                     ->searchable(),
166 188
                                 Forms\Components\Placeholder::make('total')
167 189
                                     ->hiddenLabel()
190
+                                    ->extraAttributes(['class' => 'text-left sm:text-right'])
168 191
                                     ->content(function (Forms\Get $get) {
169 192
                                         $quantity = max((float) ($get('quantity') ?? 0), 0);
170 193
                                         $unitPrice = max((float) ($get('unit_price') ?? 0), 0);
171 194
                                         $purchaseTaxes = $get('purchaseTaxes') ?? [];
172 195
                                         $purchaseDiscounts = $get('purchaseDiscounts') ?? [];
196
+                                        $currencyCode = $get('../../currency_code') ?? CurrencyAccessor::getDefaultCurrency();
173 197
 
174 198
                                         $subtotal = $quantity * $unitPrice;
175 199
 
176
-                                        // Calculate tax amount based on subtotal
177
-                                        $taxAmount = 0;
178
-                                        if (! empty($purchaseTaxes)) {
179
-                                            $taxRates = Adjustment::whereIn('id', $purchaseTaxes)->pluck('rate');
180
-                                            $taxAmount = collect($taxRates)->sum(fn ($rate) => $subtotal * ($rate / 100));
181
-                                        }
182
-
183
-                                        // Calculate discount amount based on subtotal
184
-                                        $discountAmount = 0;
185
-                                        if (! empty($purchaseDiscounts)) {
186
-                                            $discountRates = Adjustment::whereIn('id', $purchaseDiscounts)->pluck('rate');
187
-                                            $discountAmount = collect($discountRates)->sum(fn ($rate) => $subtotal * ($rate / 100));
188
-                                        }
200
+                                        $subtotalInCents = CurrencyConverter::convertToCents($subtotal, $currencyCode);
201
+
202
+                                        $taxAmountInCents = Adjustment::whereIn('id', $purchaseTaxes)
203
+                                            ->get()
204
+                                            ->sum(function (Adjustment $adjustment) use ($subtotalInCents) {
205
+                                                if ($adjustment->computation->isPercentage()) {
206
+                                                    return RateCalculator::calculatePercentage($subtotalInCents, $adjustment->getRawOriginal('rate'));
207
+                                                } else {
208
+                                                    return $adjustment->getRawOriginal('rate');
209
+                                                }
210
+                                            });
211
+
212
+                                        $discountAmountInCents = Adjustment::whereIn('id', $purchaseDiscounts)
213
+                                            ->get()
214
+                                            ->sum(function (Adjustment $adjustment) use ($subtotalInCents) {
215
+                                                if ($adjustment->computation->isPercentage()) {
216
+                                                    return RateCalculator::calculatePercentage($subtotalInCents, $adjustment->getRawOriginal('rate'));
217
+                                                } else {
218
+                                                    return $adjustment->getRawOriginal('rate');
219
+                                                }
220
+                                            });
189 221
 
190 222
                                         // Final total
191
-                                        $total = $subtotal + ($taxAmount - $discountAmount);
223
+                                        $totalInCents = $subtotalInCents + ($taxAmountInCents - $discountAmountInCents);
192 224
 
193
-                                        return CurrencyConverter::formatToMoney($total);
225
+                                        return CurrencyConverter::formatCentsToMoney($totalInCents, $currencyCode);
194 226
                                     }),
195 227
                             ]),
196
-                        BillTotals::make(),
228
+                        DocumentTotals::make()
229
+                            ->type(DocumentType::Bill),
197 230
                     ]),
198 231
             ]);
199 232
     }
@@ -225,15 +258,17 @@ class BillResource extends Resource
225 258
                 Tables\Columns\TextColumn::make('vendor.name')
226 259
                     ->sortable(),
227 260
                 Tables\Columns\TextColumn::make('total')
228
-                    ->currency()
229
-                    ->sortable(),
261
+                    ->currencyWithConversion(static fn (Bill $record) => $record->currency_code)
262
+                    ->sortable()
263
+                    ->toggleable(),
230 264
                 Tables\Columns\TextColumn::make('amount_paid')
231 265
                     ->label('Amount Paid')
232
-                    ->currency()
233
-                    ->sortable(),
266
+                    ->currencyWithConversion(static fn (Bill $record) => $record->currency_code)
267
+                    ->sortable()
268
+                    ->toggleable(),
234 269
                 Tables\Columns\TextColumn::make('amount_due')
235 270
                     ->label('Amount Due')
236
-                    ->currency()
271
+                    ->currencyWithConversion(static fn (Bill $record) => $record->currency_code)
237 272
                     ->sortable(),
238 273
             ])
239 274
             ->filters([
@@ -289,15 +324,16 @@ class BillResource extends Resource
289 324
                             Forms\Components\TextInput::make('amount')
290 325
                                 ->label('Amount')
291 326
                                 ->required()
292
-                                ->money()
327
+                                ->money(fn (Bill $record) => $record->currency_code)
293 328
                                 ->live(onBlur: true)
294 329
                                 ->helperText(function (Bill $record, $state) {
295
-                                    if (! CurrencyConverter::isValidAmount($state)) {
330
+                                    $billCurrency = $record->currency_code;
331
+                                    if (! CurrencyConverter::isValidAmount($state, $billCurrency)) {
296 332
                                         return null;
297 333
                                     }
298 334
 
299 335
                                     $amountDue = $record->getRawOriginal('amount_due');
300
-                                    $amount = CurrencyConverter::convertToCents($state);
336
+                                    $amount = CurrencyConverter::convertToCents($state, $billCurrency);
301 337
 
302 338
                                     if ($amount <= 0) {
303 339
                                         return 'Please enter a valid positive amount';
@@ -306,14 +342,14 @@ class BillResource extends Resource
306 342
                                     $newAmountDue = $amountDue - $amount;
307 343
 
308 344
                                     return match (true) {
309
-                                        $newAmountDue > 0 => 'Amount due after payment will be ' . CurrencyConverter::formatCentsToMoney($newAmountDue),
345
+                                        $newAmountDue > 0 => 'Amount due after payment will be ' . CurrencyConverter::formatCentsToMoney($newAmountDue, $billCurrency),
310 346
                                         $newAmountDue === 0 => 'Bill will be fully paid',
311
-                                        default => 'Amount exceeds bill total by ' . CurrencyConverter::formatCentsToMoney(abs($newAmountDue)),
347
+                                        default => 'Amount exceeds bill total by ' . CurrencyConverter::formatCentsToMoney(abs($newAmountDue), $billCurrency),
312 348
                                     };
313 349
                                 })
314 350
                                 ->rules([
315
-                                    static fn (): Closure => static function (string $attribute, $value, Closure $fail) {
316
-                                        if (! CurrencyConverter::isValidAmount($value)) {
351
+                                    static fn (Bill $record): Closure => static function (string $attribute, $value, Closure $fail) use ($record) {
352
+                                        if (! CurrencyConverter::isValidAmount($value, $record->currency_code)) {
317 353
                                             $fail('Please enter a valid amount');
318 354
                                         }
319 355
                                     },
@@ -395,7 +431,7 @@ class BillResource extends Resource
395 431
                             if ($cantRecordPayments) {
396 432
                                 Notification::make()
397 433
                                     ->title('Payment Recording Failed')
398
-                                    ->body('Bills that are either paid or voided cannot be processed through bulk payments. Please adjust your selection and try again.')
434
+                                    ->body('Bills that are either paid, voided, or are in a foreign currency cannot be processed through bulk payments. Please adjust your selection and try again.')
399 435
                                     ->persistent()
400 436
                                     ->danger()
401 437
                                     ->send();

+ 7
- 4
app/Filament/Company/Resources/Purchases/BillResource/Widgets/BillOverview.php View File

@@ -24,17 +24,19 @@ class BillOverview extends EnhancedStatsOverviewWidget
24 24
         $unpaidBills = $this->getPageTableQuery()
25 25
             ->whereIn('status', [BillStatus::Unpaid, BillStatus::Partial, BillStatus::Overdue]);
26 26
 
27
-        $amountToPay = $unpaidBills->sum('amount_due');
27
+        $amountToPay = $unpaidBills->get()->sumMoneyInDefaultCurrency('amount_due');
28 28
 
29 29
         $amountOverdue = $unpaidBills
30 30
             ->clone()
31 31
             ->where('status', BillStatus::Overdue)
32
-            ->sum('amount_due');
32
+            ->get()
33
+            ->sumMoneyInDefaultCurrency('amount_due');
33 34
 
34 35
         $amountDueWithin7Days = $unpaidBills
35 36
             ->clone()
36 37
             ->whereBetween('due_date', [today(), today()->addWeek()])
37
-            ->sum('amount_due');
38
+            ->get()
39
+            ->sumMoneyInDefaultCurrency('amount_due');
38 40
 
39 41
         $averagePaymentTime = $this->getPageTableQuery()
40 42
             ->whereNotNull('paid_at')
@@ -49,7 +51,8 @@ class BillOverview extends EnhancedStatsOverviewWidget
49 51
                 today()->subMonth()->startOfMonth(),
50 52
                 today()->subMonth()->endOfMonth(),
51 53
             ])
52
-            ->sum('amount_paid');
54
+            ->get()
55
+            ->sumMoneyInDefaultCurrency('amount_paid');
53 56
 
54 57
         return [
55 58
             EnhancedStatsOverviewWidget\EnhancedStat::make('Total To Pay', CurrencyConverter::formatCentsToMoney($amountToPay))

+ 0
- 1
app/Filament/Company/Resources/Purchases/VendorResource.php View File

@@ -42,7 +42,6 @@ class VendorResource extends Resource
42 42
                                     ->default(VendorType::Regular)
43 43
                                     ->columnSpanFull(),
44 44
                                 CreateCurrencySelect::make('currency_code')
45
-                                    ->relationship('currency', 'name')
46 45
                                     ->nullable()
47 46
                                     ->visible(fn (Forms\Get $get) => VendorType::parse($get('type')) === VendorType::Regular),
48 47
                                 Forms\Components\Select::make('contractor_type')

+ 1
- 2
app/Filament/Company/Resources/Sales/ClientResource.php View File

@@ -167,8 +167,7 @@ class ClientResource extends Resource
167 167
                     ])->columns(1),
168 168
                 Forms\Components\Section::make('Billing')
169 169
                     ->schema([
170
-                        CreateCurrencySelect::make('currency_code')
171
-                            ->relationship('currency', 'name'),
170
+                        CreateCurrencySelect::make('currency_code'),
172 171
                         CustomSection::make('Billing Address')
173 172
                             ->relationship('billingAddress')
174 173
                             ->contained(false)

+ 67
- 35
app/Filament/Company/Resources/Sales/InvoiceResource.php View File

@@ -2,21 +2,26 @@
2 2
 
3 3
 namespace App\Filament\Company\Resources\Sales;
4 4
 
5
-use App\Collections\Accounting\InvoiceCollection;
5
+use App\Collections\Accounting\DocumentCollection;
6 6
 use App\Enums\Accounting\DocumentDiscountMethod;
7
+use App\Enums\Accounting\DocumentType;
7 8
 use App\Enums\Accounting\InvoiceStatus;
8 9
 use App\Enums\Accounting\PaymentMethod;
9 10
 use App\Filament\Company\Resources\Sales\InvoiceResource\Pages;
10 11
 use App\Filament\Company\Resources\Sales\InvoiceResource\RelationManagers;
11 12
 use App\Filament\Company\Resources\Sales\InvoiceResource\Widgets;
12
-use App\Filament\Forms\Components\InvoiceTotals;
13
+use App\Filament\Forms\Components\CreateCurrencySelect;
14
+use App\Filament\Forms\Components\DocumentTotals;
13 15
 use App\Filament\Tables\Actions\ReplicateBulkAction;
14 16
 use App\Filament\Tables\Filters\DateRangeFilter;
15 17
 use App\Models\Accounting\Adjustment;
16 18
 use App\Models\Accounting\Invoice;
17 19
 use App\Models\Banking\BankAccount;
20
+use App\Models\Common\Client;
18 21
 use App\Models\Common\Offering;
22
+use App\Utilities\Currency\CurrencyAccessor;
19 23
 use App\Utilities\Currency\CurrencyConverter;
24
+use App\Utilities\RateCalculator;
20 25
 use Awcodes\TableRepeater\Components\TableRepeater;
21 26
 use Awcodes\TableRepeater\Header;
22 27
 use Closure;
@@ -97,7 +102,20 @@ class InvoiceResource extends Resource
97 102
                                     ->relationship('client', 'name')
98 103
                                     ->preload()
99 104
                                     ->searchable()
100
-                                    ->required(),
105
+                                    ->required()
106
+                                    ->live()
107
+                                    ->afterStateUpdated(function (Forms\Set $set, Forms\Get $get, $state) {
108
+                                        if (! $state) {
109
+                                            return;
110
+                                        }
111
+
112
+                                        $currencyCode = Client::find($state)?->currency_code;
113
+
114
+                                        if ($currencyCode) {
115
+                                            $set('currency_code', $currencyCode);
116
+                                        }
117
+                                    }),
118
+                                CreateCurrencySelect::make('currency_code'),
101 119
                             ]),
102 120
                             Forms\Components\Group::make([
103 121
                                 Forms\Components\TextInput::make('invoice_number')
@@ -222,35 +240,46 @@ class InvoiceResource extends Resource
222 240
                                     ->searchable(),
223 241
                                 Forms\Components\Placeholder::make('total')
224 242
                                     ->hiddenLabel()
243
+                                    ->extraAttributes(['class' => 'text-left sm:text-right'])
225 244
                                     ->content(function (Forms\Get $get) {
226 245
                                         $quantity = max((float) ($get('quantity') ?? 0), 0);
227 246
                                         $unitPrice = max((float) ($get('unit_price') ?? 0), 0);
228 247
                                         $salesTaxes = $get('salesTaxes') ?? [];
229 248
                                         $salesDiscounts = $get('salesDiscounts') ?? [];
249
+                                        $currencyCode = $get('../../currency_code') ?? CurrencyAccessor::getDefaultCurrency();
230 250
 
231 251
                                         $subtotal = $quantity * $unitPrice;
232 252
 
233
-                                        // Calculate tax amount based on subtotal
234
-                                        $taxAmount = 0;
235
-                                        if (! empty($salesTaxes)) {
236
-                                            $taxRates = Adjustment::whereIn('id', $salesTaxes)->pluck('rate');
237
-                                            $taxAmount = collect($taxRates)->sum(fn ($rate) => $subtotal * ($rate / 100));
238
-                                        }
239
-
240
-                                        // Calculate discount amount based on subtotal
241
-                                        $discountAmount = 0;
242
-                                        if (! empty($salesDiscounts)) {
243
-                                            $discountRates = Adjustment::whereIn('id', $salesDiscounts)->pluck('rate');
244
-                                            $discountAmount = collect($discountRates)->sum(fn ($rate) => $subtotal * ($rate / 100));
245
-                                        }
253
+                                        $subtotalInCents = CurrencyConverter::convertToCents($subtotal, $currencyCode);
254
+
255
+                                        $taxAmountInCents = Adjustment::whereIn('id', $salesTaxes)
256
+                                            ->get()
257
+                                            ->sum(function (Adjustment $adjustment) use ($subtotalInCents) {
258
+                                                if ($adjustment->computation->isPercentage()) {
259
+                                                    return RateCalculator::calculatePercentage($subtotalInCents, $adjustment->getRawOriginal('rate'));
260
+                                                } else {
261
+                                                    return $adjustment->getRawOriginal('rate');
262
+                                                }
263
+                                            });
264
+
265
+                                        $discountAmountInCents = Adjustment::whereIn('id', $salesDiscounts)
266
+                                            ->get()
267
+                                            ->sum(function (Adjustment $adjustment) use ($subtotalInCents) {
268
+                                                if ($adjustment->computation->isPercentage()) {
269
+                                                    return RateCalculator::calculatePercentage($subtotalInCents, $adjustment->getRawOriginal('rate'));
270
+                                                } else {
271
+                                                    return $adjustment->getRawOriginal('rate');
272
+                                                }
273
+                                            });
246 274
 
247 275
                                         // Final total
248
-                                        $total = $subtotal + ($taxAmount - $discountAmount);
276
+                                        $totalInCents = $subtotalInCents + ($taxAmountInCents - $discountAmountInCents);
249 277
 
250
-                                        return CurrencyConverter::formatToMoney($total);
278
+                                        return CurrencyConverter::formatCentsToMoney($totalInCents, $currencyCode);
251 279
                                     }),
252 280
                             ]),
253
-                        InvoiceTotals::make(),
281
+                        DocumentTotals::make()
282
+                            ->type(DocumentType::Invoice),
254 283
                         Forms\Components\Textarea::make('terms')
255 284
                             ->columnSpanFull(),
256 285
                     ]),
@@ -291,15 +320,17 @@ class InvoiceResource extends Resource
291 320
                     ->sortable()
292 321
                     ->searchable(),
293 322
                 Tables\Columns\TextColumn::make('total')
294
-                    ->currency()
295
-                    ->sortable(),
323
+                    ->currencyWithConversion(static fn (Invoice $record) => $record->currency_code)
324
+                    ->sortable()
325
+                    ->toggleable(),
296 326
                 Tables\Columns\TextColumn::make('amount_paid')
297 327
                     ->label('Amount Paid')
298
-                    ->currency()
299
-                    ->sortable(),
328
+                    ->currencyWithConversion(static fn (Invoice $record) => $record->currency_code)
329
+                    ->sortable()
330
+                    ->toggleable(),
300 331
                 Tables\Columns\TextColumn::make('amount_due')
301 332
                     ->label('Amount Due')
302
-                    ->currency()
333
+                    ->currencyWithConversion(static fn (Invoice $record) => $record->currency_code)
303 334
                     ->sortable(),
304 335
             ])
305 336
             ->filters([
@@ -357,16 +388,17 @@ class InvoiceResource extends Resource
357 388
                             Forms\Components\TextInput::make('amount')
358 389
                                 ->label('Amount')
359 390
                                 ->required()
360
-                                ->money()
391
+                                ->money(fn (Invoice $record) => $record->currency_code)
361 392
                                 ->live(onBlur: true)
362 393
                                 ->helperText(function (Invoice $record, $state) {
363
-                                    if (! CurrencyConverter::isValidAmount($state)) {
394
+                                    $invoiceCurrency = $record->currency_code;
395
+                                    if (! CurrencyConverter::isValidAmount($state, $invoiceCurrency)) {
364 396
                                         return null;
365 397
                                     }
366 398
 
367 399
                                     $amountDue = $record->getRawOriginal('amount_due');
368 400
 
369
-                                    $amount = CurrencyConverter::convertToCents($state);
401
+                                    $amount = CurrencyConverter::convertToCents($state, $invoiceCurrency);
370 402
 
371 403
                                     if ($amount <= 0) {
372 404
                                         return 'Please enter a valid positive amount';
@@ -379,14 +411,14 @@ class InvoiceResource extends Resource
379 411
                                     }
380 412
 
381 413
                                     return match (true) {
382
-                                        $newAmountDue > 0 => 'Amount due after payment will be ' . CurrencyConverter::formatCentsToMoney($newAmountDue),
414
+                                        $newAmountDue > 0 => 'Amount due after payment will be ' . CurrencyConverter::formatCentsToMoney($newAmountDue, $invoiceCurrency),
383 415
                                         $newAmountDue === 0 => 'Invoice will be fully paid',
384
-                                        default => 'Invoice will be overpaid by ' . CurrencyConverter::formatCentsToMoney(abs($newAmountDue)),
416
+                                        default => 'Invoice will be overpaid by ' . CurrencyConverter::formatCentsToMoney(abs($newAmountDue), $invoiceCurrency),
385 417
                                     };
386 418
                                 })
387 419
                                 ->rules([
388
-                                    static fn (): Closure => static function (string $attribute, $value, Closure $fail) {
389
-                                        if (! CurrencyConverter::isValidAmount($value)) {
420
+                                    static fn (Invoice $record): Closure => static function (string $attribute, $value, Closure $fail) use ($record) {
421
+                                        if (! CurrencyConverter::isValidAmount($value, $record->currency_code)) {
390 422
                                             $fail('Please enter a valid amount');
391 423
                                         }
392 424
                                     },
@@ -523,7 +555,7 @@ class InvoiceResource extends Resource
523 555
                             if ($cantRecordPayments) {
524 556
                                 Notification::make()
525 557
                                     ->title('Payment Recording Failed')
526
-                                    ->body('Invoices that are either draft, paid, overpaid, or voided cannot be processed through bulk payments. Please adjust your selection and try again.')
558
+                                    ->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.')
527 559
                                     ->persistent()
528 560
                                     ->danger()
529 561
                                     ->send();
@@ -531,7 +563,7 @@ class InvoiceResource extends Resource
531 563
                                 $action->cancel(true);
532 564
                             }
533 565
                         })
534
-                        ->mountUsing(function (InvoiceCollection $records, Form $form) {
566
+                        ->mountUsing(function (DocumentCollection $records, Form $form) {
535 567
                             $totalAmountDue = $records->sumMoneyFormattedSimple('amount_due');
536 568
 
537 569
                             $form->fill([
@@ -567,7 +599,7 @@ class InvoiceResource extends Resource
567 599
                             Forms\Components\Textarea::make('notes')
568 600
                                 ->label('Notes'),
569 601
                         ])
570
-                        ->before(function (InvoiceCollection $records, Tables\Actions\BulkAction $action, array $data) {
602
+                        ->before(function (DocumentCollection $records, Tables\Actions\BulkAction $action, array $data) {
571 603
                             $totalPaymentAmount = CurrencyConverter::convertToCents($data['amount']);
572 604
                             $totalAmountDue = $records->sumMoneyInCents('amount_due');
573 605
 
@@ -584,7 +616,7 @@ class InvoiceResource extends Resource
584 616
                                 $action->halt(true);
585 617
                             }
586 618
                         })
587
-                        ->action(function (InvoiceCollection $records, Tables\Actions\BulkAction $action, array $data) {
619
+                        ->action(function (DocumentCollection $records, Tables\Actions\BulkAction $action, array $data) {
588 620
                             $totalPaymentAmount = CurrencyConverter::convertToCents($data['amount']);
589 621
 
590 622
                             $remainingAmount = $totalPaymentAmount;

+ 1
- 1
app/Filament/Company/Resources/Sales/InvoiceResource/Pages/ViewInvoice.php View File

@@ -72,7 +72,7 @@ class ViewInvoice extends ViewRecord
72 72
                                     ->url(static fn (Invoice $record) => ClientResource::getUrl('edit', ['record' => $record->client_id])),
73 73
                                 TextEntry::make('amount_due')
74 74
                                     ->label('Amount Due')
75
-                                    ->money(),
75
+                                    ->currency(static fn (Invoice $record) => $record->currency_code),
76 76
                                 TextEntry::make('due_date')
77 77
                                     ->label('Due')
78 78
                                     ->asRelativeDay(),

+ 6
- 4
app/Filament/Company/Resources/Sales/InvoiceResource/Widgets/InvoiceOverview.php View File

@@ -23,17 +23,19 @@ class InvoiceOverview extends EnhancedStatsOverviewWidget
23 23
     {
24 24
         $unpaidInvoices = $this->getPageTableQuery()->unpaid();
25 25
 
26
-        $amountUnpaid = $unpaidInvoices->sum('amount_due');
26
+        $amountUnpaid = $unpaidInvoices->get()->sumMoneyInDefaultCurrency('amount_due');
27 27
 
28 28
         $amountOverdue = $unpaidInvoices
29 29
             ->clone()
30 30
             ->where('status', InvoiceStatus::Overdue)
31
-            ->sum('amount_due');
31
+            ->get()
32
+            ->sumMoneyInDefaultCurrency('amount_due');
32 33
 
33 34
         $amountDueWithin30Days = $unpaidInvoices
34 35
             ->clone()
35 36
             ->whereBetween('due_date', [today(), today()->addMonth()])
36
-            ->sum('amount_due');
37
+            ->get()
38
+            ->sumMoneyInDefaultCurrency('amount_due');
37 39
 
38 40
         $validInvoices = $this->getPageTableQuery()
39 41
             ->whereNotIn('status', [
@@ -41,7 +43,7 @@ class InvoiceOverview extends EnhancedStatsOverviewWidget
41 43
                 InvoiceStatus::Draft,
42 44
             ]);
43 45
 
44
-        $totalValidInvoiceAmount = $validInvoices->sum('total');
46
+        $totalValidInvoiceAmount = $validInvoices->get()->sumMoneyInDefaultCurrency('total');
45 47
 
46 48
         $totalValidInvoiceCount = $validInvoices->count();
47 49
 

+ 0
- 37
app/Filament/Forms/Components/BillTotals.php View File

@@ -1,37 +0,0 @@
1
-<?php
2
-
3
-namespace App\Filament\Forms\Components;
4
-
5
-use App\Enums\Accounting\AdjustmentComputation;
6
-use Filament\Forms\Components\Grid;
7
-use Filament\Forms\Components\Select;
8
-use Filament\Forms\Components\TextInput;
9
-use Filament\Forms\Get;
10
-
11
-class BillTotals extends Grid
12
-{
13
-    protected string $view = 'filament.forms.components.bill-totals';
14
-
15
-    protected function setUp(): void
16
-    {
17
-        parent::setUp();
18
-
19
-        $this->schema([
20
-            TextInput::make('discount_rate')
21
-                ->label('Discount Rate')
22
-                ->hiddenLabel()
23
-                ->live()
24
-                ->rate(computation: static fn (Get $get) => $get('discount_computation'), showAffix: false),
25
-            Select::make('discount_computation')
26
-                ->label('Discount Computation')
27
-                ->hiddenLabel()
28
-                ->options([
29
-                    'percentage' => '%',
30
-                    'fixed' => '$',
31
-                ])
32
-                ->default(AdjustmentComputation::Percentage)
33
-                ->selectablePlaceholder(false)
34
-                ->live(),
35
-        ]);
36
-    }
37
-}

+ 15
- 10
app/Filament/Forms/Components/CreateCurrencySelect.php View File

@@ -26,6 +26,20 @@ class CreateCurrencySelect extends Select
26 26
             ->required()
27 27
             ->createOptionForm($this->createCurrencyForm())
28 28
             ->createOptionAction(fn (Action $action) => $this->createCurrencyAction($action));
29
+
30
+        $this->relationship('currency', 'name');
31
+
32
+        $this->createOptionUsing(static function (array $data) {
33
+            return DB::transaction(static function () use ($data) {
34
+                $currency = CreateCurrency::create(
35
+                    $data['code'],
36
+                    $data['name'],
37
+                    $data['rate']
38
+                );
39
+
40
+                return $currency->code;
41
+            });
42
+        });
29 43
     }
30 44
 
31 45
     protected function createCurrencyForm(): array
@@ -56,15 +70,6 @@ class CreateCurrencySelect extends Select
56 70
         return $action
57 71
             ->label('Add Currency')
58 72
             ->slideOver()
59
-            ->modalWidth(MaxWidth::Medium)
60
-            ->action(static function (array $data) {
61
-                return DB::transaction(static function () use ($data) {
62
-                    $code = $data['code'];
63
-                    $name = $data['name'];
64
-                    $rate = $data['rate'];
65
-
66
-                    return CreateCurrency::create($code, $name, $rate);
67
-                });
68
-            });
73
+            ->modalWidth(MaxWidth::Medium);
69 74
     }
70 75
 }

+ 57
- 0
app/Filament/Forms/Components/DocumentTotals.php View File

@@ -0,0 +1,57 @@
1
+<?php
2
+
3
+namespace App\Filament\Forms\Components;
4
+
5
+use App\Enums\Accounting\AdjustmentComputation;
6
+use App\Enums\Accounting\DocumentType;
7
+use Filament\Forms\Components\Grid;
8
+use Filament\Forms\Components\Select;
9
+use Filament\Forms\Components\TextInput;
10
+use Filament\Forms\Get;
11
+
12
+class DocumentTotals extends Grid
13
+{
14
+    protected string $view = 'filament.forms.components.document-totals';
15
+
16
+    protected DocumentType $documentType = DocumentType::Invoice;
17
+
18
+    protected function setUp(): void
19
+    {
20
+        parent::setUp();
21
+
22
+        $this->schema([
23
+            Select::make('discount_computation')
24
+                ->label('Discount Computation')
25
+                ->hiddenLabel()
26
+                ->options(AdjustmentComputation::class)
27
+                ->default(AdjustmentComputation::Percentage)
28
+                ->selectablePlaceholder(false)
29
+                ->live(),
30
+            TextInput::make('discount_rate')
31
+                ->label('Discount Rate')
32
+                ->hiddenLabel()
33
+                ->live()
34
+                ->extraInputAttributes(['class' => 'text-right'])
35
+                ->rate(
36
+                    computation: static fn (Get $get) => $get('discount_computation'),
37
+                    currency: static fn (Get $get) => $get('currency_code'),
38
+                ),
39
+        ]);
40
+    }
41
+
42
+    public function type(DocumentType | string $type): static
43
+    {
44
+        if (is_string($type)) {
45
+            $type = DocumentType::from($type);
46
+        }
47
+
48
+        $this->documentType = $type;
49
+
50
+        return $this;
51
+    }
52
+
53
+    public function getType(): DocumentType
54
+    {
55
+        return $this->documentType;
56
+    }
57
+}

+ 0
- 37
app/Filament/Forms/Components/InvoiceTotals.php View File

@@ -1,37 +0,0 @@
1
-<?php
2
-
3
-namespace App\Filament\Forms\Components;
4
-
5
-use App\Enums\Accounting\AdjustmentComputation;
6
-use Filament\Forms\Components\Grid;
7
-use Filament\Forms\Components\Select;
8
-use Filament\Forms\Components\TextInput;
9
-use Filament\Forms\Get;
10
-
11
-class InvoiceTotals extends Grid
12
-{
13
-    protected string $view = 'filament.forms.components.invoice-totals';
14
-
15
-    protected function setUp(): void
16
-    {
17
-        parent::setUp();
18
-
19
-        $this->schema([
20
-            TextInput::make('discount_rate')
21
-                ->label('Discount Rate')
22
-                ->hiddenLabel()
23
-                ->live()
24
-                ->rate(computation: static fn (Get $get) => $get('discount_computation'), showAffix: false),
25
-            Select::make('discount_computation')
26
-                ->label('Discount Computation')
27
-                ->hiddenLabel()
28
-                ->options([
29
-                    'percentage' => '%',
30
-                    'fixed' => '$',
31
-                ])
32
-                ->default(AdjustmentComputation::Percentage)
33
-                ->selectablePlaceholder(false)
34
-                ->live(),
35
-        ]);
36
-    }
37
-}

+ 29
- 35
app/Helpers/format.php View File

@@ -1,5 +1,6 @@
1 1
 <?php
2 2
 
3
+use App\Enums\Accounting\AdjustmentComputation;
3 4
 use App\Enums\Setting\NumberFormat;
4 5
 use App\Models\Setting\Localization;
5 6
 use Filament\Support\RawJs;
@@ -44,18 +45,15 @@ if (! function_exists('percentMask')) {
44 45
 if (! function_exists('ratePrefix')) {
45 46
     function ratePrefix($computation, ?string $currency = null): ?string
46 47
     {
47
-        if ($computation instanceof BackedEnum) {
48
-            $computation = $computation->value;
49
-        }
48
+        $computationEnum = AdjustmentComputation::parse($computation);
49
+        $localization = Localization::firstOrFail();
50 50
 
51
-        if ($computation === 'fixed') {
52
-            return currency($currency)->getCodePrefix();
51
+        if ($computationEnum->isFixed() && currency($currency)->isSymbolFirst()) {
52
+            return currency($currency)->getPrefix();
53 53
         }
54 54
 
55
-        if ($computation === 'percentage' || $computation === 'compound') {
56
-            $percent_first = Localization::firstOrFail()->percent_first;
57
-
58
-            return $percent_first ? '%' : null;
55
+        if ($computationEnum->isPercentage() && $localization->percent_first) {
56
+            return '%';
59 57
         }
60 58
 
61 59
         return null;
@@ -65,18 +63,15 @@ if (! function_exists('ratePrefix')) {
65 63
 if (! function_exists('rateSuffix')) {
66 64
     function rateSuffix($computation, ?string $currency = null): ?string
67 65
     {
68
-        if ($computation instanceof BackedEnum) {
69
-            $computation = $computation->value;
70
-        }
71
-
72
-        if ($computation === 'percentage' || $computation === 'compound') {
73
-            $percent_first = Localization::firstOrFail()->percent_first;
66
+        $computationEnum = AdjustmentComputation::parse($computation);
67
+        $localization = Localization::firstOrFail();
74 68
 
75
-            return $percent_first ? null : '%';
69
+        if ($computationEnum->isFixed() && ! currency($currency)->isSymbolFirst()) {
70
+            return currency($currency)->getSuffix();
76 71
         }
77 72
 
78
-        if ($computation === 'fixed') {
79
-            return currency($currency)->getCodeSuffix();
73
+        if ($computationEnum->isPercentage() && ! $localization->percent_first) {
74
+            return '%';
80 75
         }
81 76
 
82 77
         return null;
@@ -84,19 +79,21 @@ if (! function_exists('rateSuffix')) {
84 79
 }
85 80
 
86 81
 if (! function_exists('rateMask')) {
87
-    function rateMask($computation, ?string $currency = null): RawJs
82
+    function rateMask($computation, ?string $currency = null): ?RawJs
88 83
     {
89
-        if ($computation instanceof BackedEnum) {
90
-            $computation = $computation->value;
91
-        }
84
+        $computationEnum = AdjustmentComputation::parse($computation);
92 85
 
93
-        if ($computation === 'percentage' || $computation === 'compound') {
86
+        if ($computationEnum->isPercentage()) {
94 87
             return percentMask(4);
95 88
         }
96 89
 
97
-        $precision = currency($currency)->getPrecision();
90
+        if ($computationEnum->isFixed()) {
91
+            $precision = currency($currency)->getPrecision();
98 92
 
99
-        return RawJs::make(generateJsCode($precision, $currency));
93
+            return RawJs::make(generateJsCode($precision, $currency));
94
+        }
95
+
96
+        return null;
100 97
     }
101 98
 }
102 99
 
@@ -107,21 +104,18 @@ if (! function_exists('rateFormat')) {
107 104
             return null;
108 105
         }
109 106
 
110
-        if ($computation instanceof BackedEnum) {
111
-            $computation = $computation->value;
112
-        }
113
-
114
-        if ($computation === 'percentage' || $computation === 'compound') {
115
-            $percent_first = Localization::firstOrFail()->percent_first;
107
+        $computationEnum = AdjustmentComputation::parse($computation);
108
+        $localization = Localization::firstOrFail();
116 109
 
117
-            if ($percent_first) {
118
-                return '%' . $state;
119
-            }
110
+        if ($computationEnum->isPercentage() && $localization->percent_first) {
111
+            return '%' . $state;
112
+        }
120 113
 
114
+        if ($computationEnum->isPercentage() && ! $localization->percent_first) {
121 115
             return $state . '%';
122 116
         }
123 117
 
124
-        if ($computation === 'fixed') {
118
+        if ($computationEnum->isFixed()) {
125 119
             return money($state, $currency, true)->formatWithCode();
126 120
         }
127 121
 

+ 71
- 13
app/Models/Accounting/Bill.php View File

@@ -4,6 +4,7 @@ namespace App\Models\Accounting;
4 4
 
5 5
 use App\Casts\MoneyCast;
6 6
 use App\Casts\RateCast;
7
+use App\Collections\Accounting\DocumentCollection;
7 8
 use App\Concerns\Blamable;
8 9
 use App\Concerns\CompanyOwned;
9 10
 use App\Enums\Accounting\AdjustmentComputation;
@@ -12,11 +13,15 @@ use App\Enums\Accounting\DocumentDiscountMethod;
12 13
 use App\Enums\Accounting\JournalEntryType;
13 14
 use App\Enums\Accounting\TransactionType;
14 15
 use App\Filament\Company\Resources\Purchases\BillResource;
16
+use App\Models\Banking\BankAccount;
15 17
 use App\Models\Common\Vendor;
18
+use App\Models\Setting\Currency;
16 19
 use App\Observers\BillObserver;
20
+use App\Utilities\Currency\CurrencyAccessor;
17 21
 use App\Utilities\Currency\CurrencyConverter;
18 22
 use Filament\Actions\MountableAction;
19 23
 use Filament\Actions\ReplicateAction;
24
+use Illuminate\Database\Eloquent\Attributes\CollectedBy;
20 25
 use Illuminate\Database\Eloquent\Attributes\ObservedBy;
21 26
 use Illuminate\Database\Eloquent\Builder;
22 27
 use Illuminate\Database\Eloquent\Casts\Attribute;
@@ -28,6 +33,7 @@ use Illuminate\Database\Eloquent\Relations\MorphOne;
28 33
 use Illuminate\Support\Carbon;
29 34
 
30 35
 #[ObservedBy(BillObserver::class)]
36
+#[CollectedBy(DocumentCollection::class)]
31 37
 class Bill extends Model
32 38
 {
33 39
     use Blamable;
@@ -75,6 +81,11 @@ class Bill extends Model
75 81
         'amount_due' => MoneyCast::class,
76 82
     ];
77 83
 
84
+    public function currency(): BelongsTo
85
+    {
86
+        return $this->belongsTo(Currency::class, 'currency_code', 'code');
87
+    }
88
+
78 89
     public function vendor(): BelongsTo
79 90
     {
80 91
         return $this->belongsTo(Vendor::class);
@@ -128,7 +139,7 @@ class Bill extends Model
128 139
         return ! in_array($this->status, [
129 140
             BillStatus::Paid,
130 141
             BillStatus::Void,
131
-        ]);
142
+        ]) && $this->currency_code === CurrencyAccessor::getDefaultCurrency();
132 143
     }
133 144
 
134 145
     public function hasPayments(): bool
@@ -188,13 +199,35 @@ class Bill extends Model
188 199
         $transactionType = TransactionType::Withdrawal;
189 200
         $transactionDescription = "Bill #{$this->bill_number}: Payment to {$this->vendor->name}";
190 201
 
191
-        // Create transaction
202
+        // Add multi-currency handling
203
+        $bankAccount = BankAccount::findOrFail($data['bank_account_id']);
204
+        $bankAccountCurrency = $bankAccount->account->currency_code ?? CurrencyAccessor::getDefaultCurrency();
205
+
206
+        $billCurrency = $this->currency_code;
207
+        $requiresConversion = $billCurrency !== $bankAccountCurrency;
208
+
209
+        if ($requiresConversion) {
210
+            $amountInBillCurrencyCents = CurrencyConverter::convertToCents($data['amount'], $billCurrency);
211
+            $amountInBankCurrencyCents = CurrencyConverter::convertBalance(
212
+                $amountInBillCurrencyCents,
213
+                $billCurrency,
214
+                $bankAccountCurrency
215
+            );
216
+            $formattedAmountForBankCurrency = CurrencyConverter::convertCentsToFormatSimple(
217
+                $amountInBankCurrencyCents,
218
+                $bankAccountCurrency
219
+            );
220
+        } else {
221
+            $formattedAmountForBankCurrency = $data['amount']; // Already in simple format
222
+        }
223
+
224
+        // Create transaction with converted amount
192 225
         $this->transactions()->create([
193 226
             'company_id' => $this->company_id,
194 227
             'type' => $transactionType,
195 228
             'is_payment' => true,
196 229
             'posted_at' => $data['posted_at'],
197
-            'amount' => $data['amount'],
230
+            'amount' => $formattedAmountForBankCurrency,
198 231
             'payment_method' => $data['payment_method'],
199 232
             'bank_account_id' => $data['bank_account_id'],
200 233
             'account_id' => Account::getAccountsPayableAccount()->id,
@@ -207,11 +240,13 @@ class Bill extends Model
207 240
     {
208 241
         $postedAt ??= $this->date;
209 242
 
243
+        $total = $this->formatAmountToDefaultCurrency($this->getRawOriginal('total'));
244
+
210 245
         $transaction = $this->transactions()->create([
211 246
             'company_id' => $this->company_id,
212 247
             'type' => TransactionType::Journal,
213 248
             'posted_at' => $postedAt,
214
-            'amount' => $this->total,
249
+            'amount' => $total,
215 250
             'description' => 'Bill Creation for Bill #' . $this->bill_number,
216 251
         ]);
217 252
 
@@ -221,32 +256,36 @@ class Bill extends Model
221 256
             'company_id' => $this->company_id,
222 257
             'type' => JournalEntryType::Credit,
223 258
             'account_id' => Account::getAccountsPayableAccount()->id,
224
-            'amount' => $this->total,
259
+            'amount' => $total,
225 260
             'description' => $baseDescription,
226 261
         ]);
227 262
 
228
-        $totalLineItemSubtotal = (int) $this->lineItems()->sum('subtotal');
229
-        $billDiscountTotalCents = (int) $this->getRawOriginal('discount_total');
263
+        $totalLineItemSubtotalCents = $this->convertAmountToDefaultCurrency((int) $this->lineItems()->sum('subtotal'));
264
+        $billDiscountTotalCents = $this->convertAmountToDefaultCurrency((int) $this->getRawOriginal('discount_total'));
230 265
         $remainingDiscountCents = $billDiscountTotalCents;
231 266
 
232 267
         foreach ($this->lineItems as $index => $lineItem) {
233 268
             $lineItemDescription = "{$baseDescription} › {$lineItem->offering->name}";
234 269
 
270
+            $lineItemSubtotal = $this->formatAmountToDefaultCurrency($lineItem->getRawOriginal('subtotal'));
271
+
235 272
             $transaction->journalEntries()->create([
236 273
                 'company_id' => $this->company_id,
237 274
                 'type' => JournalEntryType::Debit,
238 275
                 'account_id' => $lineItem->offering->expense_account_id,
239
-                'amount' => $lineItem->subtotal,
276
+                'amount' => $lineItemSubtotal,
240 277
                 'description' => $lineItemDescription,
241 278
             ]);
242 279
 
243 280
             foreach ($lineItem->adjustments as $adjustment) {
281
+                $adjustmentAmount = $this->formatAmountToDefaultCurrency($lineItem->calculateAdjustmentTotalAmount($adjustment));
282
+
244 283
                 if ($adjustment->isNonRecoverablePurchaseTax()) {
245 284
                     $transaction->journalEntries()->create([
246 285
                         'company_id' => $this->company_id,
247 286
                         'type' => JournalEntryType::Debit,
248 287
                         'account_id' => $lineItem->offering->expense_account_id,
249
-                        'amount' => $lineItem->calculateAdjustmentTotal($adjustment)->getAmount(),
288
+                        'amount' => $adjustmentAmount,
250 289
                         'description' => "{$lineItemDescription} ({$adjustment->name})",
251 290
                     ]);
252 291
                 } elseif ($adjustment->account_id) {
@@ -254,20 +293,20 @@ class Bill extends Model
254 293
                         'company_id' => $this->company_id,
255 294
                         'type' => $adjustment->category->isDiscount() ? JournalEntryType::Credit : JournalEntryType::Debit,
256 295
                         'account_id' => $adjustment->account_id,
257
-                        'amount' => $lineItem->calculateAdjustmentTotal($adjustment)->getAmount(),
296
+                        'amount' => $adjustmentAmount,
258 297
                         'description' => $lineItemDescription,
259 298
                     ]);
260 299
                 }
261 300
             }
262 301
 
263
-            if ($this->discount_method->isPerDocument() && $totalLineItemSubtotal > 0) {
264
-                $lineItemSubtotalCents = (int) $lineItem->getRawOriginal('subtotal');
302
+            if ($this->discount_method->isPerDocument() && $totalLineItemSubtotalCents > 0) {
303
+                $lineItemSubtotalCents = $this->convertAmountToDefaultCurrency((int) $lineItem->getRawOriginal('subtotal'));
265 304
 
266 305
                 if ($index === $this->lineItems->count() - 1) {
267 306
                     $lineItemDiscount = $remainingDiscountCents;
268 307
                 } else {
269 308
                     $lineItemDiscount = (int) round(
270
-                        ($lineItemSubtotalCents / $totalLineItemSubtotal) * $billDiscountTotalCents
309
+                        ($lineItemSubtotalCents / $totalLineItemSubtotalCents) * $billDiscountTotalCents
271 310
                     );
272 311
                     $remainingDiscountCents -= $lineItemDiscount;
273 312
                 }
@@ -296,6 +335,25 @@ class Bill extends Model
296 335
         $this->createInitialTransaction();
297 336
     }
298 337
 
338
+    public function convertAmountToDefaultCurrency(int $amountCents): int
339
+    {
340
+        $defaultCurrency = CurrencyAccessor::getDefaultCurrency();
341
+        $needsConversion = $this->currency_code !== $defaultCurrency;
342
+
343
+        if ($needsConversion) {
344
+            return CurrencyConverter::convertBalance($amountCents, $this->currency_code, $defaultCurrency);
345
+        }
346
+
347
+        return $amountCents;
348
+    }
349
+
350
+    public function formatAmountToDefaultCurrency(int $amountCents): string
351
+    {
352
+        $convertedCents = $this->convertAmountToDefaultCurrency($amountCents);
353
+
354
+        return CurrencyConverter::convertCentsToFormatSimple($convertedCents);
355
+    }
356
+
299 357
     public static function getReplicateAction(string $action = ReplicateAction::class): MountableAction
300 358
     {
301 359
         return $action::make()

+ 44
- 0
app/Models/Accounting/DocumentLineItem.php View File

@@ -12,6 +12,7 @@ use App\Enums\Accounting\AdjustmentType;
12 12
 use App\Models\Common\Offering;
13 13
 use App\Observers\DocumentLineItemObserver;
14 14
 use App\Utilities\Currency\CurrencyAccessor;
15
+use App\Utilities\RateCalculator;
15 16
 use Illuminate\Database\Eloquent\Attributes\ObservedBy;
16 17
 use Illuminate\Database\Eloquent\Factories\HasFactory;
17 18
 use Illuminate\Database\Eloquent\Model;
@@ -113,6 +114,21 @@ class DocumentLineItem extends Model
113 114
         );
114 115
     }
115 116
 
117
+    public function calculateTaxTotalAmount(): int
118
+    {
119
+        $subtotalInCents = $this->getRawOriginal('subtotal');
120
+
121
+        return $this->taxes->reduce(function (int $carry, Adjustment $tax) use ($subtotalInCents) {
122
+            if ($tax->computation->isPercentage()) {
123
+                $scaledRate = $tax->getRawOriginal('rate');
124
+
125
+                return $carry + RateCalculator::calculatePercentage($subtotalInCents, $scaledRate);
126
+            } else {
127
+                return $carry + $tax->getRawOriginal('rate');
128
+            }
129
+        }, 0);
130
+    }
131
+
116 132
     public function calculateDiscountTotal(): Money
117 133
     {
118 134
         $subtotal = money($this->subtotal, CurrencyAccessor::getDefaultCurrency());
@@ -123,10 +139,38 @@ class DocumentLineItem extends Model
123 139
         );
124 140
     }
125 141
 
142
+    public function calculateDiscountTotalAmount(): int
143
+    {
144
+        $subtotalInCents = $this->getRawOriginal('subtotal');
145
+
146
+        return $this->discounts->reduce(function (int $carry, Adjustment $discount) use ($subtotalInCents) {
147
+            if ($discount->computation->isPercentage()) {
148
+                $scaledRate = $discount->getRawOriginal('rate');
149
+
150
+                return $carry + RateCalculator::calculatePercentage($subtotalInCents, $scaledRate);
151
+            } else {
152
+                return $carry + $discount->getRawOriginal('rate');
153
+            }
154
+        }, 0);
155
+    }
156
+
126 157
     public function calculateAdjustmentTotal(Adjustment $adjustment): Money
127 158
     {
128 159
         $subtotal = money($this->subtotal, CurrencyAccessor::getDefaultCurrency());
129 160
 
130 161
         return $subtotal->multiply($adjustment->rate / 100);
131 162
     }
163
+
164
+    public function calculateAdjustmentTotalAmount(Adjustment $adjustment): int
165
+    {
166
+        $subtotalInCents = $this->getRawOriginal('subtotal');
167
+
168
+        if ($adjustment->computation->isPercentage()) {
169
+            $scaledRate = $adjustment->getRawOriginal('rate');
170
+
171
+            return RateCalculator::calculatePercentage($subtotalInCents, $scaledRate);
172
+        } else {
173
+            return $adjustment->getRawOriginal('rate');
174
+        }
175
+    }
132 176
 }

+ 67
- 13
app/Models/Accounting/Invoice.php View File

@@ -4,7 +4,7 @@ namespace App\Models\Accounting;
4 4
 
5 5
 use App\Casts\MoneyCast;
6 6
 use App\Casts\RateCast;
7
-use App\Collections\Accounting\InvoiceCollection;
7
+use App\Collections\Accounting\DocumentCollection;
8 8
 use App\Concerns\Blamable;
9 9
 use App\Concerns\CompanyOwned;
10 10
 use App\Enums\Accounting\AdjustmentComputation;
@@ -13,8 +13,11 @@ use App\Enums\Accounting\InvoiceStatus;
13 13
 use App\Enums\Accounting\JournalEntryType;
14 14
 use App\Enums\Accounting\TransactionType;
15 15
 use App\Filament\Company\Resources\Sales\InvoiceResource;
16
+use App\Models\Banking\BankAccount;
16 17
 use App\Models\Common\Client;
18
+use App\Models\Setting\Currency;
17 19
 use App\Observers\InvoiceObserver;
20
+use App\Utilities\Currency\CurrencyAccessor;
18 21
 use App\Utilities\Currency\CurrencyConverter;
19 22
 use Filament\Actions\Action;
20 23
 use Filament\Actions\MountableAction;
@@ -31,7 +34,7 @@ use Illuminate\Database\Eloquent\Relations\MorphOne;
31 34
 use Illuminate\Support\Carbon;
32 35
 
33 36
 #[ObservedBy(InvoiceObserver::class)]
34
-#[CollectedBy(InvoiceCollection::class)]
37
+#[CollectedBy(DocumentCollection::class)]
35 38
 class Invoice extends Model
36 39
 {
37 40
     use Blamable;
@@ -92,6 +95,11 @@ class Invoice extends Model
92 95
         return $this->belongsTo(Client::class);
93 96
     }
94 97
 
98
+    public function currency(): BelongsTo
99
+    {
100
+        return $this->belongsTo(Currency::class, 'currency_code', 'code');
101
+    }
102
+
95 103
     public function lineItems(): MorphMany
96 104
     {
97 105
         return $this->morphMany(DocumentLineItem::class, 'documentable');
@@ -161,7 +169,7 @@ class Invoice extends Model
161 169
             InvoiceStatus::Paid,
162 170
             InvoiceStatus::Void,
163 171
             InvoiceStatus::Overpaid,
164
-        ]);
172
+        ]) && $this->currency_code === CurrencyAccessor::getDefaultCurrency();
165 173
     }
166 174
 
167 175
     public function canBeOverdue(): bool
@@ -219,13 +227,34 @@ class Invoice extends Model
219 227
             $transactionDescription = "Invoice #{$this->invoice_number}: Payment from {$this->client->name}";
220 228
         }
221 229
 
230
+        $bankAccount = BankAccount::findOrFail($data['bank_account_id']);
231
+        $bankAccountCurrency = $bankAccount->account->currency_code ?? CurrencyAccessor::getDefaultCurrency();
232
+
233
+        $invoiceCurrency = $this->currency_code;
234
+        $requiresConversion = $invoiceCurrency !== $bankAccountCurrency;
235
+
236
+        if ($requiresConversion) {
237
+            $amountInInvoiceCurrencyCents = CurrencyConverter::convertToCents($data['amount'], $invoiceCurrency);
238
+            $amountInBankCurrencyCents = CurrencyConverter::convertBalance(
239
+                $amountInInvoiceCurrencyCents,
240
+                $invoiceCurrency,
241
+                $bankAccountCurrency
242
+            );
243
+            $formattedAmountForBankCurrency = CurrencyConverter::convertCentsToFormatSimple(
244
+                $amountInBankCurrencyCents,
245
+                $bankAccountCurrency
246
+            );
247
+        } else {
248
+            $formattedAmountForBankCurrency = $data['amount']; // Already in simple format
249
+        }
250
+
222 251
         // Create transaction
223 252
         $this->transactions()->create([
224 253
             'company_id' => $this->company_id,
225 254
             'type' => $transactionType,
226 255
             'is_payment' => true,
227 256
             'posted_at' => $data['posted_at'],
228
-            'amount' => $data['amount'],
257
+            'amount' => $formattedAmountForBankCurrency,
229 258
             'payment_method' => $data['payment_method'],
230 259
             'bank_account_id' => $data['bank_account_id'],
231 260
             'account_id' => Account::getAccountsReceivableAccount()->id,
@@ -252,11 +281,13 @@ class Invoice extends Model
252 281
 
253 282
     public function createApprovalTransaction(): void
254 283
     {
284
+        $total = $this->formatAmountToDefaultCurrency($this->getRawOriginal('total'));
285
+
255 286
         $transaction = $this->transactions()->create([
256 287
             'company_id' => $this->company_id,
257 288
             'type' => TransactionType::Journal,
258 289
             'posted_at' => $this->date,
259
-            'amount' => $this->total,
290
+            'amount' => $total,
260 291
             'description' => 'Invoice Approval for Invoice #' . $this->invoice_number,
261 292
         ]);
262 293
 
@@ -266,43 +297,47 @@ class Invoice extends Model
266 297
             'company_id' => $this->company_id,
267 298
             'type' => JournalEntryType::Debit,
268 299
             'account_id' => Account::getAccountsReceivableAccount()->id,
269
-            'amount' => $this->total,
300
+            'amount' => $total,
270 301
             'description' => $baseDescription,
271 302
         ]);
272 303
 
273
-        $totalLineItemSubtotal = (int) $this->lineItems()->sum('subtotal');
274
-        $invoiceDiscountTotalCents = (int) $this->getRawOriginal('discount_total');
304
+        $totalLineItemSubtotalCents = $this->convertAmountToDefaultCurrency((int) $this->lineItems()->sum('subtotal'));
305
+        $invoiceDiscountTotalCents = $this->convertAmountToDefaultCurrency((int) $this->getRawOriginal('discount_total'));
275 306
         $remainingDiscountCents = $invoiceDiscountTotalCents;
276 307
 
277 308
         foreach ($this->lineItems as $index => $lineItem) {
278 309
             $lineItemDescription = "{$baseDescription} › {$lineItem->offering->name}";
279 310
 
311
+            $lineItemSubtotal = $this->formatAmountToDefaultCurrency($lineItem->getRawOriginal('subtotal'));
312
+
280 313
             $transaction->journalEntries()->create([
281 314
                 'company_id' => $this->company_id,
282 315
                 'type' => JournalEntryType::Credit,
283 316
                 'account_id' => $lineItem->offering->income_account_id,
284
-                'amount' => $lineItem->subtotal,
317
+                'amount' => $lineItemSubtotal,
285 318
                 'description' => $lineItemDescription,
286 319
             ]);
287 320
 
288 321
             foreach ($lineItem->adjustments as $adjustment) {
322
+                $adjustmentAmount = $this->formatAmountToDefaultCurrency($lineItem->calculateAdjustmentTotalAmount($adjustment));
323
+
289 324
                 $transaction->journalEntries()->create([
290 325
                     'company_id' => $this->company_id,
291 326
                     'type' => $adjustment->category->isDiscount() ? JournalEntryType::Debit : JournalEntryType::Credit,
292 327
                     'account_id' => $adjustment->account_id,
293
-                    'amount' => $lineItem->calculateAdjustmentTotal($adjustment)->getAmount(),
328
+                    'amount' => $adjustmentAmount,
294 329
                     'description' => $lineItemDescription,
295 330
                 ]);
296 331
             }
297 332
 
298
-            if ($this->discount_method->isPerDocument() && $totalLineItemSubtotal > 0) {
299
-                $lineItemSubtotalCents = (int) $lineItem->getRawOriginal('subtotal');
333
+            if ($this->discount_method->isPerDocument() && $totalLineItemSubtotalCents > 0) {
334
+                $lineItemSubtotalCents = $this->convertAmountToDefaultCurrency((int) $lineItem->getRawOriginal('subtotal'));
300 335
 
301 336
                 if ($index === $this->lineItems->count() - 1) {
302 337
                     $lineItemDiscount = $remainingDiscountCents;
303 338
                 } else {
304 339
                     $lineItemDiscount = (int) round(
305
-                        ($lineItemSubtotalCents / $totalLineItemSubtotal) * $invoiceDiscountTotalCents
340
+                        ($lineItemSubtotalCents / $totalLineItemSubtotalCents) * $invoiceDiscountTotalCents
306 341
                     );
307 342
                     $remainingDiscountCents -= $lineItemDiscount;
308 343
                 }
@@ -331,6 +366,25 @@ class Invoice extends Model
331 366
         $this->createApprovalTransaction();
332 367
     }
333 368
 
369
+    public function convertAmountToDefaultCurrency(int $amountCents): int
370
+    {
371
+        $defaultCurrency = CurrencyAccessor::getDefaultCurrency();
372
+        $needsConversion = $this->currency_code !== $defaultCurrency;
373
+
374
+        if ($needsConversion) {
375
+            return CurrencyConverter::convertBalance($amountCents, $this->currency_code, $defaultCurrency);
376
+        }
377
+
378
+        return $amountCents;
379
+    }
380
+
381
+    public function formatAmountToDefaultCurrency(int $amountCents): string
382
+    {
383
+        $convertedCents = $this->convertAmountToDefaultCurrency($amountCents);
384
+
385
+        return CurrencyConverter::convertCentsToFormatSimple($convertedCents);
386
+    }
387
+
334 388
     public static function getApproveDraftAction(string $action = Action::class): MountableAction
335 389
     {
336 390
         return $action::make('approveDraft')

+ 38
- 15
app/Observers/TransactionObserver.php View File

@@ -107,21 +107,35 @@ class TransactionObserver
107 107
             return;
108 108
         }
109 109
 
110
-        $depositTotal = (int) $invoice->deposits()
110
+        $invoiceCurrency = $invoice->currency_code;
111
+
112
+        $depositTotalInInvoiceCurrencyCents = (int) $invoice->deposits()
111 113
             ->when($excludedTransaction, fn (Builder $query) => $query->whereKeyNot($excludedTransaction->getKey()))
112
-            ->sum('amount');
114
+            ->get()
115
+            ->sum(function (Transaction $transaction) use ($invoiceCurrency) {
116
+                $bankAccountCurrency = $transaction->bankAccount->account->currency_code;
117
+                $amountCents = (int) $transaction->getRawOriginal('amount');
118
+
119
+                return CurrencyConverter::convertBalance($amountCents, $bankAccountCurrency, $invoiceCurrency);
120
+            });
113 121
 
114
-        $withdrawalTotal = (int) $invoice->withdrawals()
122
+        $withdrawalTotalInInvoiceCurrencyCents = (int) $invoice->withdrawals()
115 123
             ->when($excludedTransaction, fn (Builder $query) => $query->whereKeyNot($excludedTransaction->getKey()))
116
-            ->sum('amount');
124
+            ->get()
125
+            ->sum(function (Transaction $transaction) use ($invoiceCurrency) {
126
+                $bankAccountCurrency = $transaction->bankAccount->account->currency_code;
127
+                $amountCents = (int) $transaction->getRawOriginal('amount');
128
+
129
+                return CurrencyConverter::convertBalance($amountCents, $bankAccountCurrency, $invoiceCurrency);
130
+            });
117 131
 
118
-        $totalPaid = $depositTotal - $withdrawalTotal;
132
+        $totalPaidInInvoiceCurrencyCents = $depositTotalInInvoiceCurrencyCents - $withdrawalTotalInInvoiceCurrencyCents;
119 133
 
120
-        $invoiceTotal = (int) $invoice->getRawOriginal('total');
134
+        $invoiceTotalInInvoiceCurrencyCents = (int) $invoice->getRawOriginal('total');
121 135
 
122 136
         $newStatus = match (true) {
123
-            $totalPaid > $invoiceTotal => InvoiceStatus::Overpaid,
124
-            $totalPaid === $invoiceTotal => InvoiceStatus::Paid,
137
+            $totalPaidInInvoiceCurrencyCents > $invoiceTotalInInvoiceCurrencyCents => InvoiceStatus::Overpaid,
138
+            $totalPaidInInvoiceCurrencyCents === $invoiceTotalInInvoiceCurrencyCents => InvoiceStatus::Paid,
125 139
             default => InvoiceStatus::Partial,
126 140
         };
127 141
 
@@ -134,7 +148,7 @@ class TransactionObserver
134 148
         }
135 149
 
136 150
         $invoice->update([
137
-            'amount_paid' => CurrencyConverter::convertCentsToFloat($totalPaid),
151
+            'amount_paid' => CurrencyConverter::convertCentsToFormatSimple($totalPaidInInvoiceCurrencyCents, $invoiceCurrency),
138 152
             'status' => $newStatus,
139 153
             'paid_at' => $paidAt,
140 154
         ]);
@@ -146,15 +160,24 @@ class TransactionObserver
146 160
             return;
147 161
         }
148 162
 
149
-        $withdrawalTotal = (int) $bill->withdrawals()
163
+        $billCurrency = $bill->currency_code;
164
+
165
+        $withdrawalTotalInBillCurrencyCents = (int) $bill->withdrawals()
150 166
             ->when($excludedTransaction, fn (Builder $query) => $query->whereKeyNot($excludedTransaction->getKey()))
151
-            ->sum('amount');
167
+            ->get()
168
+            ->sum(function (Transaction $transaction) use ($billCurrency) {
169
+                $bankAccountCurrency = $transaction->bankAccount->account->currency_code;
170
+                $amountCents = (int) $transaction->getRawOriginal('amount');
171
+
172
+                return CurrencyConverter::convertBalance($amountCents, $bankAccountCurrency, $billCurrency);
173
+            });
174
+
175
+        $totalPaidInBillCurrencyCents = $withdrawalTotalInBillCurrencyCents;
152 176
 
153
-        $totalPaid = $withdrawalTotal;
154
-        $billTotal = (int) $bill->getRawOriginal('total');
177
+        $billTotalInBillCurrencyCents = (int) $bill->getRawOriginal('total');
155 178
 
156 179
         $newStatus = match (true) {
157
-            $totalPaid >= $billTotal => BillStatus::Paid,
180
+            $totalPaidInBillCurrencyCents >= $billTotalInBillCurrencyCents => BillStatus::Paid,
158 181
             default => BillStatus::Partial,
159 182
         };
160 183
 
@@ -167,7 +190,7 @@ class TransactionObserver
167 190
         }
168 191
 
169 192
         $bill->update([
170
-            'amount_paid' => CurrencyConverter::convertCentsToFloat($totalPaid),
193
+            'amount_paid' => CurrencyConverter::convertCentsToFormatSimple($totalPaidInBillCurrencyCents, $billCurrency),
171 194
             'status' => $newStatus,
172 195
             'paid_at' => $paidAt,
173 196
         ]);

+ 130
- 34
app/Providers/MacroServiceProvider.php View File

@@ -4,11 +4,13 @@ namespace App\Providers;
4 4
 
5 5
 use Akaunting\Money\Currency;
6 6
 use Akaunting\Money\Money;
7
+use App\Enums\Accounting\AdjustmentComputation;
7 8
 use App\Enums\Setting\DateFormat;
8 9
 use App\Models\Accounting\AccountSubtype;
9 10
 use App\Models\Setting\Localization;
10 11
 use App\Utilities\Accounting\AccountCode;
11 12
 use App\Utilities\Currency\CurrencyAccessor;
13
+use App\Utilities\Currency\CurrencyConverter;
12 14
 use BackedEnum;
13 15
 use Carbon\CarbonInterface;
14 16
 use Closure;
@@ -61,6 +63,56 @@ class MacroServiceProvider extends ServiceProvider
61 63
             return $this;
62 64
         });
63 65
 
66
+        TextInput::macro('rate', function (string | Closure | null $computation = null, string | Closure | null $currency = null, bool $showAffix = true): static {
67
+            return $this
68
+                ->when(
69
+                    $showAffix,
70
+                    fn (TextInput $component) => $component
71
+                        ->prefix(function (TextInput $component) use ($computation, $currency) {
72
+                            $evaluatedComputation = $component->evaluate($computation);
73
+                            $evaluatedCurrency = $component->evaluate($currency);
74
+
75
+                            return ratePrefix($evaluatedComputation, $evaluatedCurrency);
76
+                        })
77
+                        ->suffix(function (TextInput $component) use ($computation, $currency) {
78
+                            $evaluatedComputation = $component->evaluate($computation);
79
+                            $evaluatedCurrency = $component->evaluate($currency);
80
+
81
+                            return rateSuffix($evaluatedComputation, $evaluatedCurrency);
82
+                        })
83
+                )
84
+                ->mask(static function (TextInput $component) use ($computation, $currency) {
85
+                    $computation = $component->evaluate($computation);
86
+                    $currency = $component->evaluate($currency);
87
+
88
+                    $computationEnum = AdjustmentComputation::parse($computation);
89
+
90
+                    if ($computationEnum->isPercentage()) {
91
+                        return rateMask(computation: $computation);
92
+                    }
93
+
94
+                    return moneyMask($currency);
95
+                })
96
+                ->rule(static function (TextInput $component) use ($computation) {
97
+                    return static function (string $attribute, $value, Closure $fail) use ($computation, $component) {
98
+                        $computation = $component->evaluate($computation);
99
+                        $numericValue = (float) $value;
100
+
101
+                        if ($computation instanceof BackedEnum) {
102
+                            $computation = $computation->value;
103
+                        }
104
+
105
+                        if ($computation === 'percentage' || $computation === 'compound') {
106
+                            if ($numericValue < 0 || $numericValue > 100) {
107
+                                $fail(translate('The rate must be between 0 and 100.'));
108
+                            }
109
+                        } elseif ($computation === 'fixed' && $numericValue < 0) {
110
+                            $fail(translate('The rate must be greater than 0.'));
111
+                        }
112
+                    };
113
+                });
114
+        });
115
+
64 116
         TextColumn::macro('defaultDateFormat', function (): static {
65 117
             $localization = Localization::firstOrFail();
66 118
 
@@ -102,45 +154,89 @@ class MacroServiceProvider extends ServiceProvider
102 154
             return $this;
103 155
         });
104 156
 
105
-        TextInput::macro('rate', function (string | Closure | null $computation = null, bool $showAffix = true): static {
106
-            $this
107
-                ->when(
108
-                    $showAffix,
109
-                    fn (TextInput $component) => $component
110
-                        ->prefix(static function (TextInput $component) use ($computation) {
111
-                            $computation = $component->evaluate($computation);
157
+        TextEntry::macro('currency', function (string | Closure | null $currency = null, ?bool $convert = null): static {
158
+            $currency ??= CurrencyAccessor::getDefaultCurrency();
159
+            $convert ??= true;
112 160
 
113
-                            return ratePrefix(computation: $computation);
114
-                        })
115
-                        ->suffix(static function (TextInput $component) use ($computation) {
116
-                            $computation = $component->evaluate($computation);
161
+            $this->formatStateUsing(static function (TextEntry $entry, $state) use ($currency, $convert): ?string {
162
+                if (blank($state)) {
163
+                    return null;
164
+                }
117 165
 
118
-                            return rateSuffix(computation: $computation);
119
-                        })
120
-                )
121
-                ->mask(static function (TextInput $component) use ($computation) {
122
-                    $computation = $component->evaluate($computation);
166
+                $currency = $entry->evaluate($currency);
167
+                $convert = $entry->evaluate($convert);
123 168
 
124
-                    return rateMask(computation: $computation);
125
-                })
126
-                ->rule(static function (TextInput $component) use ($computation) {
127
-                    return static function (string $attribute, $value, Closure $fail) use ($computation, $component) {
128
-                        $computation = $component->evaluate($computation);
129
-                        $numericValue = (float) $value;
169
+                return money($state, $currency, $convert)->format();
170
+            });
130 171
 
131
-                        if ($computation instanceof BackedEnum) {
132
-                            $computation = $computation->value;
133
-                        }
172
+            return $this;
173
+        });
134 174
 
135
-                        if ($computation === 'percentage' || $computation === 'compound') {
136
-                            if ($numericValue < 0 || $numericValue > 100) {
137
-                                $fail(translate('The rate must be between 0 and 100.'));
138
-                            }
139
-                        } elseif ($computation === 'fixed' && $numericValue < 0) {
140
-                            $fail(translate('The rate must be greater than 0.'));
141
-                        }
142
-                    };
143
-                });
175
+        TextColumn::macro('currencyWithConversion', function (string | Closure | null $currency = null): static {
176
+            $currency ??= CurrencyAccessor::getDefaultCurrency();
177
+
178
+            $this->formatStateUsing(static function (TextColumn $column, $state) use ($currency): ?string {
179
+                if (blank($state)) {
180
+                    return null;
181
+                }
182
+
183
+                $currency = $column->evaluate($currency);
184
+
185
+                return CurrencyConverter::formatToMoney($state, $currency);
186
+            });
187
+
188
+            $this->description(static function (TextColumn $column, $state) use ($currency): ?string {
189
+                if (blank($state)) {
190
+                    return null;
191
+                }
192
+
193
+                $oldCurrency = $column->evaluate($currency);
194
+                $newCurrency = CurrencyAccessor::getDefaultCurrency();
195
+
196
+                if ($oldCurrency === $newCurrency) {
197
+                    return null;
198
+                }
199
+
200
+                $balanceInCents = CurrencyConverter::convertToCents($state, $oldCurrency);
201
+
202
+                $convertedBalanceInCents = CurrencyConverter::convertBalance($balanceInCents, $oldCurrency, $newCurrency);
203
+
204
+                return CurrencyConverter::formatCentsToMoney($convertedBalanceInCents, $newCurrency, true);
205
+            });
206
+
207
+            return $this;
208
+        });
209
+
210
+        TextEntry::macro('currencyWithConversion', function (string | Closure | null $currency = null): static {
211
+            $currency ??= CurrencyAccessor::getDefaultCurrency();
212
+
213
+            $this->formatStateUsing(static function (TextEntry $entry, $state) use ($currency): ?string {
214
+                if (blank($state)) {
215
+                    return null;
216
+                }
217
+
218
+                $currency = $entry->evaluate($currency);
219
+
220
+                return CurrencyConverter::formatToMoney($state, $currency);
221
+            });
222
+
223
+            $this->helperText(static function (TextEntry $entry, $state) use ($currency): ?string {
224
+                if (blank($state)) {
225
+                    return null;
226
+                }
227
+
228
+                $oldCurrency = $entry->evaluate($currency);
229
+                $newCurrency = CurrencyAccessor::getDefaultCurrency();
230
+
231
+                if ($oldCurrency === $newCurrency) {
232
+                    return null;
233
+                }
234
+
235
+                $balanceInCents = CurrencyConverter::convertToCents($state, $oldCurrency);
236
+                $convertedBalanceInCents = CurrencyConverter::convertBalance($balanceInCents, $oldCurrency, $newCurrency);
237
+
238
+                return CurrencyConverter::formatCentsToMoney($convertedBalanceInCents, $newCurrency, true);
239
+            });
144 240
 
145 241
             return $this;
146 242
         });

+ 53
- 0
app/Utilities/RateCalculator.php View File

@@ -0,0 +1,53 @@
1
+<?php
2
+
3
+namespace App\Utilities;
4
+
5
+use App\Enums\Setting\NumberFormat;
6
+use App\Models\Setting\Localization;
7
+
8
+class RateCalculator
9
+{
10
+    public const PRECISION = 4;
11
+
12
+    public const SCALING_FACTOR = 10 ** self::PRECISION;
13
+
14
+    public const PERCENTAGE_SCALING_FACTOR = self::SCALING_FACTOR * 100;
15
+
16
+    public static function calculatePercentage(int $value, int $scaledRate): int
17
+    {
18
+        return (int) round(($value * $scaledRate) / self::PERCENTAGE_SCALING_FACTOR);
19
+    }
20
+
21
+    public static function scaledRateToDecimal(int $scaledRate): float
22
+    {
23
+        return $scaledRate / self::PERCENTAGE_SCALING_FACTOR;
24
+    }
25
+
26
+    public static function decimalToScaledRate(float $decimalRate): int
27
+    {
28
+        return (int) round($decimalRate * self::PERCENTAGE_SCALING_FACTOR);
29
+    }
30
+
31
+    public static function parseLocalizedRate(string $value): int
32
+    {
33
+        $format = Localization::firstOrFail()->number_format->value;
34
+        [$decimalMark, $thousandsSeparator] = NumberFormat::from($format)->getFormattingParameters();
35
+
36
+        $floatValue = (float) str_replace([$thousandsSeparator, $decimalMark], ['', '.'], $value);
37
+
38
+        return (int) round($floatValue * self::SCALING_FACTOR);
39
+    }
40
+
41
+    public static function formatScaledRate(int $scaledRate): string
42
+    {
43
+        $format = Localization::firstOrFail()->number_format->value;
44
+        [$decimalMark, $thousandsSeparator] = NumberFormat::from($format)->getFormattingParameters();
45
+
46
+        $percentageValue = $scaledRate / self::SCALING_FACTOR;
47
+
48
+        $formatted = number_format($percentageValue, self::PRECISION, $decimalMark, $thousandsSeparator);
49
+        $formatted = rtrim($formatted, '0');
50
+
51
+        return rtrim($formatted, $decimalMark);
52
+    }
53
+}

+ 0
- 78
app/View/Models/BillTotalViewModel.php View File

@@ -1,78 +0,0 @@
1
-<?php
2
-
3
-namespace App\View\Models;
4
-
5
-use App\Enums\Accounting\AdjustmentComputation;
6
-use App\Enums\Accounting\DocumentDiscountMethod;
7
-use App\Models\Accounting\Adjustment;
8
-use App\Models\Accounting\Bill;
9
-use App\Utilities\Currency\CurrencyConverter;
10
-
11
-class BillTotalViewModel
12
-{
13
-    public function __construct(
14
-        public ?Bill $bill,
15
-        public ?array $data = null
16
-    ) {}
17
-
18
-    public function buildViewData(): array
19
-    {
20
-        $lineItems = collect($this->data['lineItems'] ?? []);
21
-
22
-        $subtotal = $lineItems->sum(static function ($item) {
23
-            $quantity = max((float) ($item['quantity'] ?? 0), 0);
24
-            $unitPrice = max((float) ($item['unit_price'] ?? 0), 0);
25
-
26
-            return $quantity * $unitPrice;
27
-        });
28
-
29
-        $taxTotal = $lineItems->reduce(function ($carry, $item) {
30
-            $quantity = max((float) ($item['quantity'] ?? 0), 0);
31
-            $unitPrice = max((float) ($item['unit_price'] ?? 0), 0);
32
-            $purchaseTaxes = $item['purchaseTaxes'] ?? [];
33
-            $lineTotal = $quantity * $unitPrice;
34
-
35
-            $taxAmount = Adjustment::whereIn('id', $purchaseTaxes)
36
-                ->pluck('rate')
37
-                ->sum(fn ($rate) => $lineTotal * ($rate / 100));
38
-
39
-            return $carry + $taxAmount;
40
-        }, 0);
41
-
42
-        // Calculate discount based on method
43
-        $discountMethod = DocumentDiscountMethod::parse($this->data['discount_method']) ?? DocumentDiscountMethod::PerLineItem;
44
-
45
-        if ($discountMethod->isPerLineItem()) {
46
-            $discountTotal = $lineItems->reduce(function ($carry, $item) {
47
-                $quantity = max((float) ($item['quantity'] ?? 0), 0);
48
-                $unitPrice = max((float) ($item['unit_price'] ?? 0), 0);
49
-                $purchaseDiscounts = $item['purchaseDiscounts'] ?? [];
50
-                $lineTotal = $quantity * $unitPrice;
51
-
52
-                $discountAmount = Adjustment::whereIn('id', $purchaseDiscounts)
53
-                    ->pluck('rate')
54
-                    ->sum(fn ($rate) => $lineTotal * ($rate / 100));
55
-
56
-                return $carry + $discountAmount;
57
-            }, 0);
58
-        } else {
59
-            $discountComputation = AdjustmentComputation::parse($this->data['discount_computation']) ?? AdjustmentComputation::Percentage;
60
-            $discountRate = (float) ($this->data['discount_rate'] ?? 0);
61
-
62
-            if ($discountComputation->isPercentage()) {
63
-                $discountTotal = $subtotal * ($discountRate / 100);
64
-            } else {
65
-                $discountTotal = $discountRate;
66
-            }
67
-        }
68
-
69
-        $grandTotal = $subtotal + ($taxTotal - $discountTotal);
70
-
71
-        return [
72
-            'subtotal' => CurrencyConverter::formatToMoney($subtotal),
73
-            'taxTotal' => CurrencyConverter::formatToMoney($taxTotal),
74
-            'discountTotal' => CurrencyConverter::formatToMoney($discountTotal),
75
-            'grandTotal' => CurrencyConverter::formatToMoney($grandTotal),
76
-        ];
77
-    }
78
-}

+ 126
- 0
app/View/Models/DocumentTotalViewModel.php View File

@@ -0,0 +1,126 @@
1
+<?php
2
+
3
+namespace App\View\Models;
4
+
5
+use App\Enums\Accounting\AdjustmentComputation;
6
+use App\Enums\Accounting\DocumentDiscountMethod;
7
+use App\Enums\Accounting\DocumentType;
8
+use App\Models\Accounting\Adjustment;
9
+use App\Utilities\Currency\CurrencyAccessor;
10
+use App\Utilities\Currency\CurrencyConverter;
11
+use App\Utilities\RateCalculator;
12
+use Illuminate\Support\Number;
13
+
14
+class DocumentTotalViewModel
15
+{
16
+    public function __construct(
17
+        public ?array $data,
18
+        public DocumentType $documentType = DocumentType::Invoice,
19
+    ) {}
20
+
21
+    public function buildViewData(): array
22
+    {
23
+        $currencyCode = $this->data['currency_code'] ?? CurrencyAccessor::getDefaultCurrency();
24
+        $defaultCurrencyCode = CurrencyAccessor::getDefaultCurrency();
25
+
26
+        $lineItems = collect($this->data['lineItems'] ?? []);
27
+
28
+        $subtotalInCents = $lineItems->sum(fn ($item) => $this->calculateLineSubtotalInCents($item, $currencyCode));
29
+
30
+        $taxTotalInCents = $this->calculateAdjustmentsTotalInCents($lineItems, $this->documentType->getTaxKey(), $currencyCode);
31
+        $discountTotalInCents = $this->calculateDiscountTotalInCents($lineItems, $subtotalInCents, $currencyCode);
32
+
33
+        $grandTotalInCents = $subtotalInCents + ($taxTotalInCents - $discountTotalInCents);
34
+
35
+        $conversionMessage = $this->buildConversionMessage($grandTotalInCents, $currencyCode, $defaultCurrencyCode);
36
+
37
+        return [
38
+            'subtotal' => CurrencyConverter::formatCentsToMoney($subtotalInCents, $currencyCode),
39
+            'taxTotal' => CurrencyConverter::formatCentsToMoney($taxTotalInCents, $currencyCode),
40
+            'discountTotal' => CurrencyConverter::formatCentsToMoney($discountTotalInCents, $currencyCode),
41
+            'grandTotal' => CurrencyConverter::formatCentsToMoney($grandTotalInCents, $currencyCode),
42
+            'conversionMessage' => $conversionMessage,
43
+        ];
44
+    }
45
+
46
+    private function calculateLineSubtotalInCents(array $item, string $currencyCode): int
47
+    {
48
+        $quantity = max((float) ($item['quantity'] ?? 0), 0);
49
+        $unitPrice = max((float) ($item['unit_price'] ?? 0), 0);
50
+
51
+        $subtotal = $quantity * $unitPrice;
52
+
53
+        return CurrencyConverter::convertToCents($subtotal, $currencyCode);
54
+    }
55
+
56
+    private function calculateAdjustmentsTotalInCents($lineItems, string $key, string $currencyCode): int
57
+    {
58
+        return $lineItems->reduce(function ($carry, $item) use ($key, $currencyCode) {
59
+            $quantity = max((float) ($item['quantity'] ?? 0), 0);
60
+            $unitPrice = max((float) ($item['unit_price'] ?? 0), 0);
61
+            $adjustmentIds = $item[$key] ?? [];
62
+            $lineTotal = $quantity * $unitPrice;
63
+
64
+            $lineTotalInCents = CurrencyConverter::convertToCents($lineTotal, $currencyCode);
65
+
66
+            $adjustmentTotal = Adjustment::whereIn('id', $adjustmentIds)
67
+                ->get()
68
+                ->sum(function (Adjustment $adjustment) use ($lineTotalInCents) {
69
+                    if ($adjustment->computation->isPercentage()) {
70
+                        return RateCalculator::calculatePercentage($lineTotalInCents, $adjustment->getRawOriginal('rate'));
71
+                    } else {
72
+                        return $adjustment->getRawOriginal('rate');
73
+                    }
74
+                });
75
+
76
+            return $carry + $adjustmentTotal;
77
+        }, 0);
78
+    }
79
+
80
+    private function calculateDiscountTotalInCents($lineItems, int $subtotalInCents, string $currencyCode): int
81
+    {
82
+        $discountMethod = DocumentDiscountMethod::parse($this->data['discount_method']) ?? DocumentDiscountMethod::PerLineItem;
83
+
84
+        if ($discountMethod->isPerLineItem()) {
85
+            return $this->calculateAdjustmentsTotalInCents($lineItems, $this->documentType->getDiscountKey(), $currencyCode);
86
+        }
87
+
88
+        $discountComputation = AdjustmentComputation::parse($this->data['discount_computation']) ?? AdjustmentComputation::Percentage;
89
+        $discountRate = blank($this->data['discount_rate']) ? '0' : $this->data['discount_rate'];
90
+
91
+        if ($discountComputation->isPercentage()) {
92
+            $scaledDiscountRate = RateCalculator::parseLocalizedRate($discountRate);
93
+
94
+            return RateCalculator::calculatePercentage($subtotalInCents, $scaledDiscountRate);
95
+        }
96
+
97
+        if (! CurrencyConverter::isValidAmount($discountRate)) {
98
+            $discountRate = '0';
99
+        }
100
+
101
+        return CurrencyConverter::convertToCents($discountRate, $currencyCode);
102
+    }
103
+
104
+    private function buildConversionMessage(int $grandTotalInCents, string $currencyCode, string $defaultCurrencyCode): ?string
105
+    {
106
+        if ($currencyCode === $defaultCurrencyCode) {
107
+            return null;
108
+        }
109
+
110
+        $rate = currency($currencyCode)->getRate();
111
+        $indirectRate = 1 / $rate;
112
+
113
+        $convertedTotalInCents = CurrencyConverter::convertBalance($grandTotalInCents, $currencyCode, $defaultCurrencyCode);
114
+
115
+        $formattedRate = Number::format($indirectRate, maxPrecision: 10);
116
+
117
+        return sprintf(
118
+            'Currency conversion: %s (%s) at an exchange rate of 1 %s = %s %s',
119
+            CurrencyConverter::formatCentsToMoney($convertedTotalInCents, $defaultCurrencyCode),
120
+            $defaultCurrencyCode,
121
+            $currencyCode,
122
+            $formattedRate,
123
+            $defaultCurrencyCode
124
+        );
125
+    }
126
+}

+ 0
- 78
app/View/Models/InvoiceTotalViewModel.php View File

@@ -1,78 +0,0 @@
1
-<?php
2
-
3
-namespace App\View\Models;
4
-
5
-use App\Enums\Accounting\AdjustmentComputation;
6
-use App\Enums\Accounting\DocumentDiscountMethod;
7
-use App\Models\Accounting\Adjustment;
8
-use App\Models\Accounting\Invoice;
9
-use App\Utilities\Currency\CurrencyConverter;
10
-
11
-class InvoiceTotalViewModel
12
-{
13
-    public function __construct(
14
-        public ?Invoice $invoice,
15
-        public ?array $data = null
16
-    ) {}
17
-
18
-    public function buildViewData(): array
19
-    {
20
-        $lineItems = collect($this->data['lineItems'] ?? []);
21
-
22
-        $subtotal = $lineItems->sum(static function ($item) {
23
-            $quantity = max((float) ($item['quantity'] ?? 0), 0);
24
-            $unitPrice = max((float) ($item['unit_price'] ?? 0), 0);
25
-
26
-            return $quantity * $unitPrice;
27
-        });
28
-
29
-        $taxTotal = $lineItems->reduce(function ($carry, $item) {
30
-            $quantity = max((float) ($item['quantity'] ?? 0), 0);
31
-            $unitPrice = max((float) ($item['unit_price'] ?? 0), 0);
32
-            $salesTaxes = $item['salesTaxes'] ?? [];
33
-            $lineTotal = $quantity * $unitPrice;
34
-
35
-            $taxAmount = Adjustment::whereIn('id', $salesTaxes)
36
-                ->pluck('rate')
37
-                ->sum(fn ($rate) => $lineTotal * ($rate / 100));
38
-
39
-            return $carry + $taxAmount;
40
-        }, 0);
41
-
42
-        // Calculate discount based on method
43
-        $discountMethod = DocumentDiscountMethod::parse($this->data['discount_method']) ?? DocumentDiscountMethod::PerLineItem;
44
-
45
-        if ($discountMethod->isPerLineItem()) {
46
-            $discountTotal = $lineItems->reduce(function ($carry, $item) {
47
-                $quantity = max((float) ($item['quantity'] ?? 0), 0);
48
-                $unitPrice = max((float) ($item['unit_price'] ?? 0), 0);
49
-                $salesDiscounts = $item['salesDiscounts'] ?? [];
50
-                $lineTotal = $quantity * $unitPrice;
51
-
52
-                $discountAmount = Adjustment::whereIn('id', $salesDiscounts)
53
-                    ->pluck('rate')
54
-                    ->sum(fn ($rate) => $lineTotal * ($rate / 100));
55
-
56
-                return $carry + $discountAmount;
57
-            }, 0);
58
-        } else {
59
-            $discountComputation = AdjustmentComputation::parse($this->data['discount_computation']) ?? AdjustmentComputation::Percentage;
60
-            $discountRate = (float) ($this->data['discount_rate'] ?? 0);
61
-
62
-            if ($discountComputation->isPercentage()) {
63
-                $discountTotal = $subtotal * ($discountRate / 100);
64
-            } else {
65
-                $discountTotal = $discountRate;
66
-            }
67
-        }
68
-
69
-        $grandTotal = $subtotal + ($taxTotal - $discountTotal);
70
-
71
-        return [
72
-            'subtotal' => CurrencyConverter::formatToMoney($subtotal),
73
-            'taxTotal' => CurrencyConverter::formatToMoney($taxTotal),
74
-            'discountTotal' => CurrencyConverter::formatToMoney($discountTotal),
75
-            'grandTotal' => CurrencyConverter::formatToMoney($grandTotal),
76
-        ];
77
-    }
78
-}

+ 1
- 1
composer.json View File

@@ -17,7 +17,7 @@
17 17
         "andrewdwallo/transmatic": "^1.1",
18 18
         "awcodes/filament-table-repeater": "^3.0",
19 19
         "barryvdh/laravel-snappy": "^1.0",
20
-        "filament/filament": "v3.2.129",
20
+        "filament/filament": "^3.2",
21 21
         "guava/filament-clusters": "^1.1",
22 22
         "guzzlehttp/guzzle": "^7.8",
23 23
         "jaocero/radio-deck": "^1.2",

+ 85
- 85
composer.lock View File

@@ -4,7 +4,7 @@
4 4
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
5 5
         "This file is @generated automatically"
6 6
     ],
7
-    "content-hash": "7301f370a98e2573adf4e6b4e7ab4a8e",
7
+    "content-hash": "a04479d57fe01a2694ad92a8901363ea",
8 8
     "packages": [
9 9
         {
10 10
             "name": "akaunting/laravel-money",
@@ -497,16 +497,16 @@
497 497
         },
498 498
         {
499 499
             "name": "aws/aws-sdk-php",
500
-            "version": "3.334.6",
500
+            "version": "3.336.0",
501 501
             "source": {
502 502
                 "type": "git",
503 503
                 "url": "https://github.com/aws/aws-sdk-php.git",
504
-                "reference": "2b0be3aded849d3b7bb0b53ea3295c7cecdeeee7"
504
+                "reference": "5a62003bf183e32da038056a4b9077c27224e034"
505 505
             },
506 506
             "dist": {
507 507
                 "type": "zip",
508
-                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/2b0be3aded849d3b7bb0b53ea3295c7cecdeeee7",
509
-                "reference": "2b0be3aded849d3b7bb0b53ea3295c7cecdeeee7",
508
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/5a62003bf183e32da038056a4b9077c27224e034",
509
+                "reference": "5a62003bf183e32da038056a4b9077c27224e034",
510 510
                 "shasum": ""
511 511
             },
512 512
             "require": {
@@ -589,9 +589,9 @@
589 589
             "support": {
590 590
                 "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
591 591
                 "issues": "https://github.com/aws/aws-sdk-php/issues",
592
-                "source": "https://github.com/aws/aws-sdk-php/tree/3.334.6"
592
+                "source": "https://github.com/aws/aws-sdk-php/tree/3.336.0"
593 593
             },
594
-            "time": "2024-12-13T19:18:29+00:00"
594
+            "time": "2024-12-18T19:04:32+00:00"
595 595
         },
596 596
         {
597 597
             "name": "aws/aws-sdk-php-laravel",
@@ -1662,16 +1662,16 @@
1662 1662
         },
1663 1663
         {
1664 1664
             "name": "filament/actions",
1665
-            "version": "v3.2.129",
1665
+            "version": "v3.2.131",
1666 1666
             "source": {
1667 1667
                 "type": "git",
1668 1668
                 "url": "https://github.com/filamentphp/actions.git",
1669
-                "reference": "1ee8b0a890b53e8b0b341134d3ba9bdaeee294d3"
1669
+                "reference": "8d9ceaf392eeff55fd335f5169d14f84af8c325e"
1670 1670
             },
1671 1671
             "dist": {
1672 1672
                 "type": "zip",
1673
-                "url": "https://api.github.com/repos/filamentphp/actions/zipball/1ee8b0a890b53e8b0b341134d3ba9bdaeee294d3",
1674
-                "reference": "1ee8b0a890b53e8b0b341134d3ba9bdaeee294d3",
1673
+                "url": "https://api.github.com/repos/filamentphp/actions/zipball/8d9ceaf392eeff55fd335f5169d14f84af8c325e",
1674
+                "reference": "8d9ceaf392eeff55fd335f5169d14f84af8c325e",
1675 1675
                 "shasum": ""
1676 1676
             },
1677 1677
             "require": {
@@ -1711,20 +1711,20 @@
1711 1711
                 "issues": "https://github.com/filamentphp/filament/issues",
1712 1712
                 "source": "https://github.com/filamentphp/filament"
1713 1713
             },
1714
-            "time": "2024-12-05T08:56:37+00:00"
1714
+            "time": "2024-12-17T13:03:16+00:00"
1715 1715
         },
1716 1716
         {
1717 1717
             "name": "filament/filament",
1718
-            "version": "v3.2.129",
1718
+            "version": "v3.2.131",
1719 1719
             "source": {
1720 1720
                 "type": "git",
1721 1721
                 "url": "https://github.com/filamentphp/panels.git",
1722
-                "reference": "9a327a54dec24e0b12c437ef799828f492cb882a"
1722
+                "reference": "21febddcc6720b250b41386805a8dbd1deef2c56"
1723 1723
             },
1724 1724
             "dist": {
1725 1725
                 "type": "zip",
1726
-                "url": "https://api.github.com/repos/filamentphp/panels/zipball/9a327a54dec24e0b12c437ef799828f492cb882a",
1727
-                "reference": "9a327a54dec24e0b12c437ef799828f492cb882a",
1726
+                "url": "https://api.github.com/repos/filamentphp/panels/zipball/21febddcc6720b250b41386805a8dbd1deef2c56",
1727
+                "reference": "21febddcc6720b250b41386805a8dbd1deef2c56",
1728 1728
                 "shasum": ""
1729 1729
             },
1730 1730
             "require": {
@@ -1776,20 +1776,20 @@
1776 1776
                 "issues": "https://github.com/filamentphp/filament/issues",
1777 1777
                 "source": "https://github.com/filamentphp/filament"
1778 1778
             },
1779
-            "time": "2024-12-11T09:05:49+00:00"
1779
+            "time": "2024-12-17T13:03:11+00:00"
1780 1780
         },
1781 1781
         {
1782 1782
             "name": "filament/forms",
1783
-            "version": "v3.2.129",
1783
+            "version": "v3.2.131",
1784 1784
             "source": {
1785 1785
                 "type": "git",
1786 1786
                 "url": "https://github.com/filamentphp/forms.git",
1787
-                "reference": "e6133bdc03de05dfe23eac386b49e51093338883"
1787
+                "reference": "72429b0df9c3d123273dd51ba62b764e2114697c"
1788 1788
             },
1789 1789
             "dist": {
1790 1790
                 "type": "zip",
1791
-                "url": "https://api.github.com/repos/filamentphp/forms/zipball/e6133bdc03de05dfe23eac386b49e51093338883",
1792
-                "reference": "e6133bdc03de05dfe23eac386b49e51093338883",
1791
+                "url": "https://api.github.com/repos/filamentphp/forms/zipball/72429b0df9c3d123273dd51ba62b764e2114697c",
1792
+                "reference": "72429b0df9c3d123273dd51ba62b764e2114697c",
1793 1793
                 "shasum": ""
1794 1794
             },
1795 1795
             "require": {
@@ -1832,20 +1832,20 @@
1832 1832
                 "issues": "https://github.com/filamentphp/filament/issues",
1833 1833
                 "source": "https://github.com/filamentphp/filament"
1834 1834
             },
1835
-            "time": "2024-12-11T09:05:38+00:00"
1835
+            "time": "2024-12-17T13:03:11+00:00"
1836 1836
         },
1837 1837
         {
1838 1838
             "name": "filament/infolists",
1839
-            "version": "v3.2.129",
1839
+            "version": "v3.2.131",
1840 1840
             "source": {
1841 1841
                 "type": "git",
1842 1842
                 "url": "https://github.com/filamentphp/infolists.git",
1843
-                "reference": "0db994330e7fe21a9684256ea1fc34fee916a8d6"
1843
+                "reference": "15c200a3172b88a6247ff4b7230f69982d848194"
1844 1844
             },
1845 1845
             "dist": {
1846 1846
                 "type": "zip",
1847
-                "url": "https://api.github.com/repos/filamentphp/infolists/zipball/0db994330e7fe21a9684256ea1fc34fee916a8d6",
1848
-                "reference": "0db994330e7fe21a9684256ea1fc34fee916a8d6",
1847
+                "url": "https://api.github.com/repos/filamentphp/infolists/zipball/15c200a3172b88a6247ff4b7230f69982d848194",
1848
+                "reference": "15c200a3172b88a6247ff4b7230f69982d848194",
1849 1849
                 "shasum": ""
1850 1850
             },
1851 1851
             "require": {
@@ -1883,11 +1883,11 @@
1883 1883
                 "issues": "https://github.com/filamentphp/filament/issues",
1884 1884
                 "source": "https://github.com/filamentphp/filament"
1885 1885
             },
1886
-            "time": "2024-12-11T09:05:38+00:00"
1886
+            "time": "2024-12-17T13:03:14+00:00"
1887 1887
         },
1888 1888
         {
1889 1889
             "name": "filament/notifications",
1890
-            "version": "v3.2.129",
1890
+            "version": "v3.2.131",
1891 1891
             "source": {
1892 1892
                 "type": "git",
1893 1893
                 "url": "https://github.com/filamentphp/notifications.git",
@@ -1939,16 +1939,16 @@
1939 1939
         },
1940 1940
         {
1941 1941
             "name": "filament/support",
1942
-            "version": "v3.2.129",
1942
+            "version": "v3.2.131",
1943 1943
             "source": {
1944 1944
                 "type": "git",
1945 1945
                 "url": "https://github.com/filamentphp/support.git",
1946
-                "reference": "e8aed9684d5c58ff7dde9517c7f1af44d575d871"
1946
+                "reference": "ddc16d8da50d73f7300671b70c9dcb942d845d9d"
1947 1947
             },
1948 1948
             "dist": {
1949 1949
                 "type": "zip",
1950
-                "url": "https://api.github.com/repos/filamentphp/support/zipball/e8aed9684d5c58ff7dde9517c7f1af44d575d871",
1951
-                "reference": "e8aed9684d5c58ff7dde9517c7f1af44d575d871",
1950
+                "url": "https://api.github.com/repos/filamentphp/support/zipball/ddc16d8da50d73f7300671b70c9dcb942d845d9d",
1951
+                "reference": "ddc16d8da50d73f7300671b70c9dcb942d845d9d",
1952 1952
                 "shasum": ""
1953 1953
             },
1954 1954
             "require": {
@@ -1994,20 +1994,20 @@
1994 1994
                 "issues": "https://github.com/filamentphp/filament/issues",
1995 1995
                 "source": "https://github.com/filamentphp/filament"
1996 1996
             },
1997
-            "time": "2024-12-11T09:05:54+00:00"
1997
+            "time": "2024-12-17T13:03:15+00:00"
1998 1998
         },
1999 1999
         {
2000 2000
             "name": "filament/tables",
2001
-            "version": "v3.2.129",
2001
+            "version": "v3.2.131",
2002 2002
             "source": {
2003 2003
                 "type": "git",
2004 2004
                 "url": "https://github.com/filamentphp/tables.git",
2005
-                "reference": "acdec73ae82961654a52a22ed9f53207cfee2ef8"
2005
+                "reference": "224aea12a4a4cfcd158b53df94cdd190f8226cac"
2006 2006
             },
2007 2007
             "dist": {
2008 2008
                 "type": "zip",
2009
-                "url": "https://api.github.com/repos/filamentphp/tables/zipball/acdec73ae82961654a52a22ed9f53207cfee2ef8",
2010
-                "reference": "acdec73ae82961654a52a22ed9f53207cfee2ef8",
2009
+                "url": "https://api.github.com/repos/filamentphp/tables/zipball/224aea12a4a4cfcd158b53df94cdd190f8226cac",
2010
+                "reference": "224aea12a4a4cfcd158b53df94cdd190f8226cac",
2011 2011
                 "shasum": ""
2012 2012
             },
2013 2013
             "require": {
@@ -2046,20 +2046,20 @@
2046 2046
                 "issues": "https://github.com/filamentphp/filament/issues",
2047 2047
                 "source": "https://github.com/filamentphp/filament"
2048 2048
             },
2049
-            "time": "2024-12-11T09:05:54+00:00"
2049
+            "time": "2024-12-17T13:03:09+00:00"
2050 2050
         },
2051 2051
         {
2052 2052
             "name": "filament/widgets",
2053
-            "version": "v3.2.129",
2053
+            "version": "v3.2.131",
2054 2054
             "source": {
2055 2055
                 "type": "git",
2056 2056
                 "url": "https://github.com/filamentphp/widgets.git",
2057
-                "reference": "6de1c84d71168fd1c6a5b1ae1e1b4ec5ee4b6f55"
2057
+                "reference": "869a419fe42e2cf1b9461f2d1e702e2fcad030ae"
2058 2058
             },
2059 2059
             "dist": {
2060 2060
                 "type": "zip",
2061
-                "url": "https://api.github.com/repos/filamentphp/widgets/zipball/6de1c84d71168fd1c6a5b1ae1e1b4ec5ee4b6f55",
2062
-                "reference": "6de1c84d71168fd1c6a5b1ae1e1b4ec5ee4b6f55",
2061
+                "url": "https://api.github.com/repos/filamentphp/widgets/zipball/869a419fe42e2cf1b9461f2d1e702e2fcad030ae",
2062
+                "reference": "869a419fe42e2cf1b9461f2d1e702e2fcad030ae",
2063 2063
                 "shasum": ""
2064 2064
             },
2065 2065
             "require": {
@@ -2090,7 +2090,7 @@
2090 2090
                 "issues": "https://github.com/filamentphp/filament/issues",
2091 2091
                 "source": "https://github.com/filamentphp/filament"
2092 2092
             },
2093
-            "time": "2024-11-27T16:52:29+00:00"
2093
+            "time": "2024-12-17T13:03:07+00:00"
2094 2094
         },
2095 2095
         {
2096 2096
             "name": "firebase/php-jwt",
@@ -2980,16 +2980,16 @@
2980 2980
         },
2981 2981
         {
2982 2982
             "name": "laravel/framework",
2983
-            "version": "v11.35.1",
2983
+            "version": "v11.36.1",
2984 2984
             "source": {
2985 2985
                 "type": "git",
2986 2986
                 "url": "https://github.com/laravel/framework.git",
2987
-                "reference": "dcfa130ede1a6fa4343dc113410963e791ad34fb"
2987
+                "reference": "df06f5163f4550641fdf349ebc04916a61135a64"
2988 2988
             },
2989 2989
             "dist": {
2990 2990
                 "type": "zip",
2991
-                "url": "https://api.github.com/repos/laravel/framework/zipball/dcfa130ede1a6fa4343dc113410963e791ad34fb",
2992
-                "reference": "dcfa130ede1a6fa4343dc113410963e791ad34fb",
2991
+                "url": "https://api.github.com/repos/laravel/framework/zipball/df06f5163f4550641fdf349ebc04916a61135a64",
2992
+                "reference": "df06f5163f4550641fdf349ebc04916a61135a64",
2993 2993
                 "shasum": ""
2994 2994
             },
2995 2995
             "require": {
@@ -3010,7 +3010,7 @@
3010 3010
                 "guzzlehttp/uri-template": "^1.0",
3011 3011
                 "laravel/prompts": "^0.1.18|^0.2.0|^0.3.0",
3012 3012
                 "laravel/serializable-closure": "^1.3|^2.0",
3013
-                "league/commonmark": "^2.2.1",
3013
+                "league/commonmark": "^2.6",
3014 3014
                 "league/flysystem": "^3.25.1",
3015 3015
                 "league/flysystem-local": "^3.25.1",
3016 3016
                 "league/uri": "^7.5.1",
@@ -3025,7 +3025,7 @@
3025 3025
                 "symfony/console": "^7.0.3",
3026 3026
                 "symfony/error-handler": "^7.0.3",
3027 3027
                 "symfony/finder": "^7.0.3",
3028
-                "symfony/http-foundation": "^7.0.3",
3028
+                "symfony/http-foundation": "^7.2.0",
3029 3029
                 "symfony/http-kernel": "^7.0.3",
3030 3030
                 "symfony/mailer": "^7.0.3",
3031 3031
                 "symfony/mime": "^7.0.3",
@@ -3191,7 +3191,7 @@
3191 3191
                 "issues": "https://github.com/laravel/framework/issues",
3192 3192
                 "source": "https://github.com/laravel/framework"
3193 3193
             },
3194
-            "time": "2024-12-12T18:25:58+00:00"
3194
+            "time": "2024-12-17T22:32:08+00:00"
3195 3195
         },
3196 3196
         {
3197 3197
             "name": "laravel/prompts",
@@ -3254,16 +3254,16 @@
3254 3254
         },
3255 3255
         {
3256 3256
             "name": "laravel/sanctum",
3257
-            "version": "v4.0.6",
3257
+            "version": "v4.0.7",
3258 3258
             "source": {
3259 3259
                 "type": "git",
3260 3260
                 "url": "https://github.com/laravel/sanctum.git",
3261
-                "reference": "9e069e36d90b1e1f41886efa0fe9800a6b354694"
3261
+                "reference": "698064236a46df016e64a7eb059b1414e0b281df"
3262 3262
             },
3263 3263
             "dist": {
3264 3264
                 "type": "zip",
3265
-                "url": "https://api.github.com/repos/laravel/sanctum/zipball/9e069e36d90b1e1f41886efa0fe9800a6b354694",
3266
-                "reference": "9e069e36d90b1e1f41886efa0fe9800a6b354694",
3265
+                "url": "https://api.github.com/repos/laravel/sanctum/zipball/698064236a46df016e64a7eb059b1414e0b281df",
3266
+                "reference": "698064236a46df016e64a7eb059b1414e0b281df",
3267 3267
                 "shasum": ""
3268 3268
             },
3269 3269
             "require": {
@@ -3314,20 +3314,20 @@
3314 3314
                 "issues": "https://github.com/laravel/sanctum/issues",
3315 3315
                 "source": "https://github.com/laravel/sanctum"
3316 3316
             },
3317
-            "time": "2024-11-26T21:18:33+00:00"
3317
+            "time": "2024-12-11T16:40:21+00:00"
3318 3318
         },
3319 3319
         {
3320 3320
             "name": "laravel/serializable-closure",
3321
-            "version": "v2.0.0",
3321
+            "version": "v2.0.1",
3322 3322
             "source": {
3323 3323
                 "type": "git",
3324 3324
                 "url": "https://github.com/laravel/serializable-closure.git",
3325
-                "reference": "0d8d3d8086984996df86596a86dea60398093a81"
3325
+                "reference": "613b2d4998f85564d40497e05e89cb6d9bd1cbe8"
3326 3326
             },
3327 3327
             "dist": {
3328 3328
                 "type": "zip",
3329
-                "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/0d8d3d8086984996df86596a86dea60398093a81",
3330
-                "reference": "0d8d3d8086984996df86596a86dea60398093a81",
3329
+                "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/613b2d4998f85564d40497e05e89cb6d9bd1cbe8",
3330
+                "reference": "613b2d4998f85564d40497e05e89cb6d9bd1cbe8",
3331 3331
                 "shasum": ""
3332 3332
             },
3333 3333
             "require": {
@@ -3375,20 +3375,20 @@
3375 3375
                 "issues": "https://github.com/laravel/serializable-closure/issues",
3376 3376
                 "source": "https://github.com/laravel/serializable-closure"
3377 3377
             },
3378
-            "time": "2024-11-19T01:38:44+00:00"
3378
+            "time": "2024-12-16T15:26:28+00:00"
3379 3379
         },
3380 3380
         {
3381 3381
             "name": "laravel/socialite",
3382
-            "version": "v5.16.0",
3382
+            "version": "v5.16.1",
3383 3383
             "source": {
3384 3384
                 "type": "git",
3385 3385
                 "url": "https://github.com/laravel/socialite.git",
3386
-                "reference": "40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf"
3386
+                "reference": "4e5be83c0b3ecf81b2ffa47092e917d1f79dce71"
3387 3387
             },
3388 3388
             "dist": {
3389 3389
                 "type": "zip",
3390
-                "url": "https://api.github.com/repos/laravel/socialite/zipball/40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf",
3391
-                "reference": "40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf",
3390
+                "url": "https://api.github.com/repos/laravel/socialite/zipball/4e5be83c0b3ecf81b2ffa47092e917d1f79dce71",
3391
+                "reference": "4e5be83c0b3ecf81b2ffa47092e917d1f79dce71",
3392 3392
                 "shasum": ""
3393 3393
             },
3394 3394
             "require": {
@@ -3398,7 +3398,7 @@
3398 3398
                 "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0",
3399 3399
                 "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0",
3400 3400
                 "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0",
3401
-                "league/oauth1-client": "^1.10.1",
3401
+                "league/oauth1-client": "^1.11",
3402 3402
                 "php": "^7.2|^8.0",
3403 3403
                 "phpseclib/phpseclib": "^3.0"
3404 3404
             },
@@ -3447,7 +3447,7 @@
3447 3447
                 "issues": "https://github.com/laravel/socialite/issues",
3448 3448
                 "source": "https://github.com/laravel/socialite"
3449 3449
             },
3450
-            "time": "2024-09-03T09:46:57+00:00"
3450
+            "time": "2024-12-11T16:43:51+00:00"
3451 3451
         },
3452 3452
         {
3453 3453
             "name": "laravel/tinker",
@@ -3706,16 +3706,16 @@
3706 3706
         },
3707 3707
         {
3708 3708
             "name": "league/csv",
3709
-            "version": "9.20.0",
3709
+            "version": "9.20.1",
3710 3710
             "source": {
3711 3711
                 "type": "git",
3712 3712
                 "url": "https://github.com/thephpleague/csv.git",
3713
-                "reference": "553579df208641ada6ffb450b3a151e2fcfa4f31"
3713
+                "reference": "491d1e79e973a7370c7571dc0fe4a7241f4936ee"
3714 3714
             },
3715 3715
             "dist": {
3716 3716
                 "type": "zip",
3717
-                "url": "https://api.github.com/repos/thephpleague/csv/zipball/553579df208641ada6ffb450b3a151e2fcfa4f31",
3718
-                "reference": "553579df208641ada6ffb450b3a151e2fcfa4f31",
3717
+                "url": "https://api.github.com/repos/thephpleague/csv/zipball/491d1e79e973a7370c7571dc0fe4a7241f4936ee",
3718
+                "reference": "491d1e79e973a7370c7571dc0fe4a7241f4936ee",
3719 3719
                 "shasum": ""
3720 3720
             },
3721 3721
             "require": {
@@ -3789,7 +3789,7 @@
3789 3789
                     "type": "github"
3790 3790
                 }
3791 3791
             ],
3792
-            "time": "2024-12-13T15:49:27+00:00"
3792
+            "time": "2024-12-18T10:11:15+00:00"
3793 3793
         },
3794 3794
         {
3795 3795
             "name": "league/flysystem",
@@ -4374,16 +4374,16 @@
4374 4374
         },
4375 4375
         {
4376 4376
             "name": "matomo/device-detector",
4377
-            "version": "6.4.1",
4377
+            "version": "6.4.2",
4378 4378
             "source": {
4379 4379
                 "type": "git",
4380 4380
                 "url": "https://github.com/matomo-org/device-detector.git",
4381
-                "reference": "0d364e0dd6c177da3c24cd4049178026324fd7ac"
4381
+                "reference": "806e52d214b05ddead1a1d4304c7592f61f95976"
4382 4382
             },
4383 4383
             "dist": {
4384 4384
                 "type": "zip",
4385
-                "url": "https://api.github.com/repos/matomo-org/device-detector/zipball/0d364e0dd6c177da3c24cd4049178026324fd7ac",
4386
-                "reference": "0d364e0dd6c177da3c24cd4049178026324fd7ac",
4385
+                "url": "https://api.github.com/repos/matomo-org/device-detector/zipball/806e52d214b05ddead1a1d4304c7592f61f95976",
4386
+                "reference": "806e52d214b05ddead1a1d4304c7592f61f95976",
4387 4387
                 "shasum": ""
4388 4388
             },
4389 4389
             "require": {
@@ -4439,7 +4439,7 @@
4439 4439
                 "source": "https://github.com/matomo-org/matomo",
4440 4440
                 "wiki": "https://dev.matomo.org/"
4441 4441
             },
4442
-            "time": "2024-09-24T13:50:04+00:00"
4442
+            "time": "2024-12-16T16:38:01+00:00"
4443 4443
         },
4444 4444
         {
4445 4445
             "name": "monolog/monolog",
@@ -5065,16 +5065,16 @@
5065 5065
         },
5066 5066
         {
5067 5067
             "name": "openspout/openspout",
5068
-            "version": "v4.28.2",
5068
+            "version": "v4.28.3",
5069 5069
             "source": {
5070 5070
                 "type": "git",
5071 5071
                 "url": "https://github.com/openspout/openspout.git",
5072
-                "reference": "d6dd654b5db502f28c5773edfa785b516745a142"
5072
+                "reference": "12b5eddcc230a97a9a67a722ad75c247e1a16750"
5073 5073
             },
5074 5074
             "dist": {
5075 5075
                 "type": "zip",
5076
-                "url": "https://api.github.com/repos/openspout/openspout/zipball/d6dd654b5db502f28c5773edfa785b516745a142",
5077
-                "reference": "d6dd654b5db502f28c5773edfa785b516745a142",
5076
+                "url": "https://api.github.com/repos/openspout/openspout/zipball/12b5eddcc230a97a9a67a722ad75c247e1a16750",
5077
+                "reference": "12b5eddcc230a97a9a67a722ad75c247e1a16750",
5078 5078
                 "shasum": ""
5079 5079
             },
5080 5080
             "require": {
@@ -5094,7 +5094,7 @@
5094 5094
                 "phpstan/phpstan": "^2.0.3",
5095 5095
                 "phpstan/phpstan-phpunit": "^2.0.1",
5096 5096
                 "phpstan/phpstan-strict-rules": "^2",
5097
-                "phpunit/phpunit": "^11.4.4"
5097
+                "phpunit/phpunit": "^11.5.0"
5098 5098
             },
5099 5099
             "suggest": {
5100 5100
                 "ext-iconv": "To handle non UTF-8 CSV files (if \"php-mbstring\" is not already installed or is too limited)",
@@ -5142,7 +5142,7 @@
5142 5142
             ],
5143 5143
             "support": {
5144 5144
                 "issues": "https://github.com/openspout/openspout/issues",
5145
-                "source": "https://github.com/openspout/openspout/tree/v4.28.2"
5145
+                "source": "https://github.com/openspout/openspout/tree/v4.28.3"
5146 5146
             },
5147 5147
             "funding": [
5148 5148
                 {
@@ -5154,7 +5154,7 @@
5154 5154
                     "type": "github"
5155 5155
                 }
5156 5156
             ],
5157
-            "time": "2024-12-06T06:17:37+00:00"
5157
+            "time": "2024-12-17T11:28:11+00:00"
5158 5158
         },
5159 5159
         {
5160 5160
             "name": "paragonie/constant_time_encoding",
@@ -12817,8 +12817,8 @@
12817 12817
             "type": "library",
12818 12818
             "extra": {
12819 12819
                 "thanks": {
12820
-                    "name": "symfony/polyfill",
12821
-                    "url": "https://github.com/symfony/polyfill"
12820
+                    "url": "https://github.com/symfony/polyfill",
12821
+                    "name": "symfony/polyfill"
12822 12822
                 }
12823 12823
             },
12824 12824
             "autoload": {

+ 1
- 1
config/money.php View File

@@ -1101,7 +1101,7 @@ return [
1101 1101
             'symbol' => 'Fr.',
1102 1102
             'symbol_first' => true,
1103 1103
             'decimal_mark' => '.',
1104
-            'thousands_separator' => ',',
1104
+            'thousands_separator' => "'",
1105 1105
         ],
1106 1106
 
1107 1107
         'MOP' => [

+ 12
- 12
package-lock.json View File

@@ -1057,9 +1057,9 @@
1057 1057
             }
1058 1058
         },
1059 1059
         "node_modules/caniuse-lite": {
1060
-            "version": "1.0.30001688",
1061
-            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001688.tgz",
1062
-            "integrity": "sha512-Nmqpru91cuABu/DTCXbM2NSRHzM2uVHfPnhJ/1zEAJx/ILBRVmz3pzH4N7DZqbdG0gWClsCC05Oj0mJ/1AWMbA==",
1060
+            "version": "1.0.30001689",
1061
+            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001689.tgz",
1062
+            "integrity": "sha512-CmeR2VBycfa+5/jOfnp/NpWPGd06nf1XYiefUvhXFfZE4GkRc9jv+eGPS4nT558WS/8lYCzV8SlANCIPvbWP1g==",
1063 1063
             "dev": true,
1064 1064
             "funding": [
1065 1065
                 {
@@ -1218,9 +1218,9 @@
1218 1218
             "license": "MIT"
1219 1219
         },
1220 1220
         "node_modules/electron-to-chromium": {
1221
-            "version": "1.5.73",
1222
-            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.73.tgz",
1223
-            "integrity": "sha512-8wGNxG9tAG5KhGd3eeA0o6ixhiNdgr0DcHWm85XPCphwZgD1lIEoi6t3VERayWao7SF7AAZTw6oARGJeVjH8Kg==",
1221
+            "version": "1.5.74",
1222
+            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.74.tgz",
1223
+            "integrity": "sha512-ck3//9RC+6oss/1Bh9tiAVFy5vfSKbRHAFh7Z3/eTRkEqJeWgymloShB17Vg3Z4nmDNp35vAd1BZ6CMW4Wt6Iw==",
1224 1224
             "dev": true,
1225 1225
             "license": "ISC"
1226 1226
         },
@@ -1569,9 +1569,9 @@
1569 1569
             }
1570 1570
         },
1571 1571
         "node_modules/jiti": {
1572
-            "version": "1.21.6",
1573
-            "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz",
1574
-            "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==",
1572
+            "version": "1.21.7",
1573
+            "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz",
1574
+            "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
1575 1575
             "dev": true,
1576 1576
             "license": "MIT",
1577 1577
             "bin": {
@@ -2470,9 +2470,9 @@
2470 2470
             }
2471 2471
         },
2472 2472
         "node_modules/tailwindcss": {
2473
-            "version": "3.4.16",
2474
-            "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.16.tgz",
2475
-            "integrity": "sha512-TI4Cyx7gDiZ6r44ewaJmt0o6BrMCT5aK5e0rmJ/G9Xq3w7CX/5VXl/zIPEJZFUK5VEqwByyhqNPycPlvcK4ZNw==",
2473
+            "version": "3.4.17",
2474
+            "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.17.tgz",
2475
+            "integrity": "sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==",
2476 2476
             "dev": true,
2477 2477
             "license": "MIT",
2478 2478
             "dependencies": {

+ 7
- 7
resources/views/filament/company/resources/sales/invoices/components/invoice-view.blade.php View File

@@ -113,8 +113,8 @@
113 113
                         @endif
114 114
                     </td>
115 115
                     <td class="text-center py-3">{{ $item->quantity }}</td>
116
-                    <td class="text-right py-3">{{ CurrencyConverter::formatToMoney($item->unit_price) }}</td>
117
-                    <td class="text-right pr-6 py-3">{{ CurrencyConverter::formatToMoney($item->subtotal) }}</td>
116
+                    <td class="text-right py-3">{{ CurrencyConverter::formatToMoney($item->unit_price, $invoice->currency_code) }}</td>
117
+                    <td class="text-right pr-6 py-3">{{ CurrencyConverter::formatToMoney($item->subtotal, $invoice->currency_code) }}</td>
118 118
                 </tr>
119 119
             @endforeach
120 120
             </tbody>
@@ -122,14 +122,14 @@
122 122
             <tr>
123 123
                 <td class="pl-6 py-2" colspan="2"></td>
124 124
                 <td class="text-right font-semibold py-2">Subtotal:</td>
125
-                <td class="text-right pr-6 py-2">{{ CurrencyConverter::formatToMoney($invoice->subtotal) }}</td>
125
+                <td class="text-right pr-6 py-2">{{ CurrencyConverter::formatToMoney($invoice->subtotal, $invoice->currency_code) }}</td>
126 126
             </tr>
127 127
             @if($invoice->discount_total)
128 128
                 <tr class="text-success-800 dark:text-success-600">
129 129
                     <td class="pl-6 py-2" colspan="2"></td>
130 130
                     <td class="text-right py-2">Discount:</td>
131 131
                     <td class="text-right pr-6 py-2">
132
-                        ({{ CurrencyConverter::formatToMoney($invoice->discount_total) }})
132
+                        ({{ CurrencyConverter::formatToMoney($invoice->discount_total, $invoice->currency_code) }})
133 133
                     </td>
134 134
                 </tr>
135 135
             @endif
@@ -137,20 +137,20 @@
137 137
                 <tr>
138 138
                     <td class="pl-6 py-2" colspan="2"></td>
139 139
                     <td class="text-right py-2">Tax:</td>
140
-                    <td class="text-right pr-6 py-2">{{ CurrencyConverter::formatToMoney($invoice->tax_total) }}</td>
140
+                    <td class="text-right pr-6 py-2">{{ CurrencyConverter::formatToMoney($invoice->tax_total, $invoice->currency_code) }}</td>
141 141
                 </tr>
142 142
             @endif
143 143
             <tr>
144 144
                 <td class="pl-6 py-2" colspan="2"></td>
145 145
                 <td class="text-right font-semibold border-t py-2">Total:</td>
146
-                <td class="text-right border-t pr-6 py-2">{{ CurrencyConverter::formatToMoney($invoice->total) }}</td>
146
+                <td class="text-right border-t pr-6 py-2">{{ CurrencyConverter::formatToMoney($invoice->total, $invoice->currency_code) }}</td>
147 147
             </tr>
148 148
             <tr>
149 149
                 <td class="pl-6 py-2" colspan="2"></td>
150 150
                 <td class="text-right font-semibold border-t-4 border-double py-2">Amount Due
151 151
                     ({{ $invoice->currency_code }}):
152 152
                 </td>
153
-                <td class="text-right border-t-4 border-double pr-6 py-2">{{ CurrencyConverter::formatToMoney($invoice->amount_due) }}</td>
153
+                <td class="text-right border-t-4 border-double pr-6 py-2">{{ CurrencyConverter::formatToMoney($invoice->amount_due, $invoice->currency_code) }}</td>
154 154
             </tr>
155 155
             </tfoot>
156 156
         </table>

+ 0
- 61
resources/views/filament/forms/components/bill-totals.blade.php View File

@@ -1,61 +0,0 @@
1
-@php
2
-    use App\Enums\Accounting\DocumentDiscountMethod;
3
-    use App\Utilities\Currency\CurrencyAccessor;
4
-    use App\View\Models\BillTotalViewModel;
5
-
6
-    $data = $this->form->getRawState();
7
-    $viewModel = new BillTotalViewModel($this->record, $data);
8
-    extract($viewModel->buildViewData(), EXTR_SKIP);
9
-
10
-    $discountMethod = DocumentDiscountMethod::parse($data['discount_method']);
11
-    $isPerDocumentDiscount = $discountMethod->isPerDocument();
12
-@endphp
13
-
14
-<div class="totals-summary w-full pr-14">
15
-    <table class="w-full text-right table-fixed">
16
-        <colgroup>
17
-            <col class="w-[20%]"> {{-- Items --}}
18
-            <col class="w-[30%]"> {{-- Description --}}
19
-            <col class="w-[10%]"> {{-- Quantity --}}
20
-            <col class="w-[10%]"> {{-- Price --}}
21
-            <col class="w-[20%]"> {{-- Taxes --}}
22
-            <col class="w-[10%]"> {{-- Amount --}}
23
-        </colgroup>
24
-        <tbody>
25
-            <tr>
26
-                <td colspan="4"></td>
27
-                <td class="text-sm px-4 py-2 font-medium leading-6 text-gray-950 dark:text-white">Subtotal:</td>
28
-                <td class="text-sm pl-4 py-2 leading-6">{{ $subtotal }}</td>
29
-            </tr>
30
-            <tr>
31
-                <td colspan="4"></td>
32
-                <td class="text-sm px-4 py-2 font-medium leading-6 text-gray-950 dark:text-white">Taxes:</td>
33
-                <td class="text-sm pl-4 py-2 leading-6">{{ $taxTotal }}</td>
34
-            </tr>
35
-            @if($isPerDocumentDiscount)
36
-                <tr>
37
-                    <td colspan="4" class="text-sm px-4 py-2 font-medium leading-6 text-gray-950 dark:text-white text-right">Discount:</td>
38
-                    <td class="text-sm px-4 py-2">
39
-                        <div class="flex justify-between space-x-2">
40
-                            @foreach($getChildComponentContainer()->getComponents() as $component)
41
-                                <div class="flex-1">{{ $component }}</div>
42
-                            @endforeach
43
-                        </div>
44
-                    </td>
45
-                    <td class="text-sm pl-4 py-2 leading-6">({{ $discountTotal }})</td>
46
-                </tr>
47
-            @else
48
-                <tr>
49
-                    <td colspan="4"></td>
50
-                    <td class="text-sm px-4 py-2 font-medium leading-6 text-gray-950 dark:text-white">Discounts:</td>
51
-                    <td class="text-sm pl-4 py-2 leading-6">({{ $discountTotal }})</td>
52
-                </tr>
53
-            @endif
54
-            <tr class="font-semibold">
55
-                <td colspan="4"></td>
56
-                <td class="text-sm px-4 py-2 font-medium leading-6 text-gray-950 dark:text-white">Total:</td>
57
-                <td class="text-sm pl-4 py-2 leading-6">{{ $grandTotal }}</td>
58
-            </tr>
59
-        </tbody>
60
-    </table>
61
-</div>

+ 107
- 0
resources/views/filament/forms/components/document-totals.blade.php View File

@@ -0,0 +1,107 @@
1
+@php
2
+    use App\Enums\Accounting\DocumentDiscountMethod;
3
+    use App\Utilities\Currency\CurrencyAccessor;
4
+    use App\View\Models\DocumentTotalViewModel;
5
+
6
+    $data = $this->form->getRawState();
7
+    $type = $getType();
8
+    $viewModel = new DocumentTotalViewModel($data, $type);
9
+    extract($viewModel->buildViewData(), EXTR_SKIP);
10
+
11
+    $discountMethod = DocumentDiscountMethod::parse($data['discount_method']);
12
+    $isPerDocumentDiscount = $discountMethod->isPerDocument();
13
+@endphp
14
+
15
+<div class="totals-summary w-full sm:pr-14">
16
+    <table class="w-full text-right table-fixed hidden sm:table">
17
+        <colgroup>
18
+            <col class="w-[20%]"> {{-- Items --}}
19
+            <col class="w-[30%]"> {{-- Description --}}
20
+            <col class="w-[10%]"> {{-- Quantity --}}
21
+            <col class="w-[10%]"> {{-- Price --}}
22
+            <col class="w-[20%]"> {{-- Taxes --}}
23
+            <col class="w-[10%]"> {{-- Amount --}}
24
+        </colgroup>
25
+        <tbody>
26
+            <tr>
27
+                <td colspan="4"></td>
28
+                <td class="text-sm px-4 py-2 font-medium leading-6 text-gray-950 dark:text-white">Subtotal:</td>
29
+                <td class="text-sm pl-4 py-2 leading-6">{{ $subtotal }}</td>
30
+            </tr>
31
+            <tr>
32
+                <td colspan="4"></td>
33
+                <td class="text-sm px-4 py-2 font-medium leading-6 text-gray-950 dark:text-white">Taxes:</td>
34
+                <td class="text-sm pl-4 py-2 leading-6">{{ $taxTotal }}</td>
35
+            </tr>
36
+            @if($isPerDocumentDiscount)
37
+                <tr>
38
+                    <td colspan="3" class="text-sm px-4 py-2 font-medium leading-6 text-gray-950 dark:text-white text-right">Discount:</td>
39
+                    <td colspan="2" class="text-sm px-4 py-2">
40
+                        <div class="flex justify-between space-x-2">
41
+                            @foreach($getChildComponentContainer()->getComponents() as $component)
42
+                                <div class="flex-1 text-left">{{ $component }}</div>
43
+                            @endforeach
44
+                        </div>
45
+                    </td>
46
+                    <td class="text-sm pl-4 py-2 leading-6">({{ $discountTotal }})</td>
47
+                </tr>
48
+            @else
49
+                <tr>
50
+                    <td colspan="4"></td>
51
+                    <td class="text-sm px-4 py-2 font-medium leading-6 text-gray-950 dark:text-white">Discounts:</td>
52
+                    <td class="text-sm pl-4 py-2 leading-6">({{ $discountTotal }})</td>
53
+                </tr>
54
+            @endif
55
+            <tr class="font-semibold">
56
+                <td colspan="4"></td>
57
+                <td class="text-sm px-4 py-2 font-medium leading-6 text-gray-950 dark:text-white">Total:</td>
58
+                <td class="text-sm pl-4 py-2 leading-6">{{ $grandTotal }}</td>
59
+            </tr>
60
+            @if($conversionMessage)
61
+                <tr>
62
+                    <td colspan="6" class="text-sm pl-4 py-2 leading-6 text-gray-600">
63
+                        {{ $conversionMessage }}
64
+                    </td>
65
+                </tr>
66
+            @endif
67
+        </tbody>
68
+    </table>
69
+
70
+    <!-- Mobile View -->
71
+    <div class="block sm:hidden p-5">
72
+        <div class="flex flex-col space-y-6">
73
+            <div class="flex justify-between items-center">
74
+                <span class="text-sm font-medium text-gray-950 dark:text-white">Subtotal:</span>
75
+                <span class="text-sm text-gray-950 dark:text-white">{{ $subtotal }}</span>
76
+            </div>
77
+            <div class="flex justify-between items-center">
78
+                <span class="text-sm font-medium text-gray-950 dark:text-white">Taxes:</span>
79
+                <span class="text-sm text-gray-950 dark:text-white">{{ $taxTotal }}</span>
80
+            </div>
81
+            @if($isPerDocumentDiscount)
82
+                <div class="flex flex-col space-y-2">
83
+                    <span class="text-sm font-medium text-gray-950 dark:text-white">Discount:</span>
84
+                    <div class="flex justify-between space-x-2">
85
+                        @foreach($getChildComponentContainer()->getComponents() as $component)
86
+                            <div class="w-1/2">{{ $component }}</div>
87
+                        @endforeach
88
+                    </div>
89
+                </div>
90
+            @else
91
+                <div class="flex justify-between items-center">
92
+                    <span class="text-sm font-medium text-gray-950 dark:text-white">Discounts:</span>
93
+                    <span class="text-sm text-gray-950 dark:text-white">({{ $discountTotal }})</span>
94
+                </div>
95
+            @endif
96
+            <div class="flex justify-between items-center font-semibold">
97
+                <span class="text-sm font-medium text-gray-950 dark:text-white">Total:</span>
98
+                <span class="text-sm text-gray-950 dark:text-white">{{ $grandTotal }}</span>
99
+            </div>
100
+            @if($conversionMessage)
101
+                <div class="text-sm text-gray-600">
102
+                    {{ $conversionMessage }}
103
+                </div>
104
+            @endif
105
+        </div>
106
+    </div>
107
+</div>

+ 0
- 61
resources/views/filament/forms/components/invoice-totals.blade.php View File

@@ -1,61 +0,0 @@
1
-@php
2
-    use App\Enums\Accounting\DocumentDiscountMethod;
3
-    use App\Utilities\Currency\CurrencyAccessor;
4
-    use App\View\Models\InvoiceTotalViewModel;
5
-
6
-    $data = $this->form->getRawState();
7
-    $viewModel = new InvoiceTotalViewModel($this->record, $data);
8
-    extract($viewModel->buildViewData(), EXTR_SKIP);
9
-
10
-    $discountMethod = DocumentDiscountMethod::parse($data['discount_method']);
11
-    $isPerDocumentDiscount = $discountMethod->isPerDocument();
12
-@endphp
13
-
14
-<div class="totals-summary w-full pr-14">
15
-    <table class="w-full text-right table-fixed">
16
-        <colgroup>
17
-            <col class="w-[20%]"> {{-- Items --}}
18
-            <col class="w-[30%]"> {{-- Description --}}
19
-            <col class="w-[10%]"> {{-- Quantity --}}
20
-            <col class="w-[10%]"> {{-- Price --}}
21
-            <col class="w-[20%]"> {{-- Taxes --}}
22
-            <col class="w-[10%]"> {{-- Amount --}}
23
-        </colgroup>
24
-        <tbody>
25
-            <tr>
26
-                <td colspan="4"></td>
27
-                <td class="text-sm px-4 py-2 font-medium leading-6 text-gray-950 dark:text-white">Subtotal:</td>
28
-                <td class="text-sm pl-4 py-2 leading-6">{{ $subtotal }}</td>
29
-            </tr>
30
-            <tr>
31
-                <td colspan="4"></td>
32
-                <td class="text-sm px-4 py-2 font-medium leading-6 text-gray-950 dark:text-white">Taxes:</td>
33
-                <td class="text-sm pl-4 py-2 leading-6">{{ $taxTotal }}</td>
34
-            </tr>
35
-            @if($isPerDocumentDiscount)
36
-                <tr>
37
-                    <td colspan="4" class="text-sm px-4 py-2 font-medium leading-6 text-gray-950 dark:text-white text-right">Discount:</td>
38
-                    <td class="text-sm px-4 py-2">
39
-                        <div class="flex justify-between space-x-2">
40
-                            @foreach($getChildComponentContainer()->getComponents() as $component)
41
-                                <div class="flex-1">{{ $component }}</div>
42
-                            @endforeach
43
-                        </div>
44
-                    </td>
45
-                    <td class="text-sm pl-4 py-2 leading-6">({{ $discountTotal }})</td>
46
-                </tr>
47
-            @else
48
-                <tr>
49
-                    <td colspan="4"></td>
50
-                    <td class="text-sm px-4 py-2 font-medium leading-6 text-gray-950 dark:text-white">Discounts:</td>
51
-                    <td class="text-sm pl-4 py-2 leading-6">({{ $discountTotal }})</td>
52
-                </tr>
53
-            @endif
54
-            <tr class="font-semibold">
55
-                <td colspan="4"></td>
56
-                <td class="text-sm px-4 py-2 font-medium leading-6 text-gray-950 dark:text-white">Total:</td>
57
-                <td class="text-sm pl-4 py-2 leading-6">{{ $grandTotal }}</td>
58
-            </tr>
59
-        </tbody>
60
-    </table>
61
-</div>

Loading…
Cancel
Save