Pārlūkot izejas kodu

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

Development 3.x
3.x
Andrew Wallo 10 mēnešus atpakaļ
vecāks
revīzija
54fee607b6
Revīzijas autora e-pasta adrese nav piesaistīta nevienam kontam
36 mainītis faili ar 1125 papildinājumiem un 731 dzēšanām
  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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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 Parādīt failu

@@ -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>

Notiek ielāde…
Atcelt
Saglabāt