浏览代码

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

Development 3.x
3.x
Andrew Wallo 10 个月前
父节点
当前提交
54fee607b6
没有帐户链接到提交者的电子邮件
共有 36 个文件被更改,包括 1125 次插入731 次删除
  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 查看文件

2
 
2
 
3
 namespace App\Casts;
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
 use App\Utilities\Currency\CurrencyAccessor;
6
 use App\Utilities\Currency\CurrencyAccessor;
7
+use App\Utilities\RateCalculator;
8
 use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
8
 use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
9
 use Illuminate\Database\Eloquent\Model;
9
 use Illuminate\Database\Eloquent\Model;
10
 
10
 
11
 class RateCast implements CastsAttributes
11
 class RateCast implements CastsAttributes
12
 {
12
 {
13
-    private const PRECISION = 4;
14
-
15
     public function get($model, string $key, $value, array $attributes): string
13
     public function get($model, string $key, $value, array $attributes): string
16
     {
14
     {
15
+        if (! $value) {
16
+            return '0';
17
+        }
18
+
17
         $currency_code = $this->getDefaultCurrencyCode();
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
             return money($value, $currency_code)->formatSimple();
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
     public function set(Model $model, string $key, mixed $value, array $attributes): int
29
     public function set(Model $model, string $key, mixed $value, array $attributes): int
33
     {
30
     {
31
+        if (! $value) {
32
+            return 0;
33
+        }
34
+
34
         if (is_int($value)) {
35
         if (is_int($value)) {
35
             return $value;
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
         $currency_code = $this->getDefaultCurrencyCode();
41
         $currency_code = $this->getDefaultCurrencyCode();
41
 
42
 
42
-        if ($computation === 'fixed') {
43
+        if ($computation?->isFixed()) {
43
             return money($value, $currency_code, true)->getAmount();
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
     private function getDefaultCurrencyCode(): string
50
     private function getDefaultCurrencyCode(): string
55
     {
51
     {
56
         return CurrencyAccessor::getDefaultCurrency();
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 查看文件

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 查看文件

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 查看文件

6
 use App\Enums\Accounting\DocumentDiscountMethod;
6
 use App\Enums\Accounting\DocumentDiscountMethod;
7
 use App\Models\Accounting\Bill;
7
 use App\Models\Accounting\Bill;
8
 use App\Models\Accounting\DocumentLineItem;
8
 use App\Models\Accounting\DocumentLineItem;
9
+use App\Utilities\Currency\CurrencyAccessor;
9
 use App\Utilities\Currency\CurrencyConverter;
10
 use App\Utilities\Currency\CurrencyConverter;
11
+use App\Utilities\RateCalculator;
10
 use Illuminate\Database\Eloquent\Model;
12
 use Illuminate\Database\Eloquent\Model;
11
 use Illuminate\Support\Collection;
13
 use Illuminate\Support\Collection;
12
 
14
 
79
 
81
 
80
     protected function updateDocumentTotals(Model $record, array $data): array
82
     protected function updateDocumentTotals(Model $record, array $data): array
81
     {
83
     {
84
+        $currencyCode = $data['currency_code'] ?? CurrencyAccessor::getDefaultCurrency();
82
         $subtotalCents = $record->lineItems()->sum('subtotal');
85
         $subtotalCents = $record->lineItems()->sum('subtotal');
83
         $taxTotalCents = $record->lineItems()->sum('tax_total');
86
         $taxTotalCents = $record->lineItems()->sum('tax_total');
84
         $discountTotalCents = $this->calculateDiscountTotal(
87
         $discountTotalCents = $this->calculateDiscountTotal(
86
             AdjustmentComputation::parse($data['discount_computation']),
89
             AdjustmentComputation::parse($data['discount_computation']),
87
             $data['discount_rate'] ?? null,
90
             $data['discount_rate'] ?? null,
88
             $subtotalCents,
91
             $subtotalCents,
89
-            $record
92
+            $record,
93
+            $currencyCode,
90
         );
94
         );
91
 
95
 
92
         $grandTotalCents = $subtotalCents + $taxTotalCents - $discountTotalCents;
96
         $grandTotalCents = $subtotalCents + $taxTotalCents - $discountTotalCents;
93
 
97
 
94
         return [
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
         ?AdjustmentComputation $discountComputation,
108
         ?AdjustmentComputation $discountComputation,
105
         ?string $discountRate,
109
         ?string $discountRate,
106
         int $subtotalCents,
110
         int $subtotalCents,
107
-        Model $record
111
+        Model $record,
112
+        string $currencyCode
108
     ): int {
113
     ): int {
109
         if ($discountMethod->isPerLineItem()) {
114
         if ($discountMethod->isPerLineItem()) {
110
             return $record->lineItems()->sum('discount_total');
115
             return $record->lineItems()->sum('discount_total');
111
         }
116
         }
112
 
117
 
113
         if ($discountComputation?->isPercentage()) {
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 查看文件

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 查看文件

305
                         }
305
                         }
306
                     )
306
                     )
307
                     ->sortable()
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
             ->recordClasses(static fn (Transaction $transaction) => $transaction->reviewed ? 'bg-primary-300/10' : null)
310
             ->recordClasses(static fn (Transaction $transaction) => $transaction->reviewed ? 'bg-primary-300/10' : null)
311
             ->defaultSort('posted_at', 'desc')
311
             ->defaultSort('posted_at', 'desc')

+ 1
- 2
app/Filament/Company/Resources/Banking/AccountResource.php 查看文件

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

+ 67
- 31
app/Filament/Company/Resources/Purchases/BillResource.php 查看文件

4
 
4
 
5
 use App\Enums\Accounting\BillStatus;
5
 use App\Enums\Accounting\BillStatus;
6
 use App\Enums\Accounting\DocumentDiscountMethod;
6
 use App\Enums\Accounting\DocumentDiscountMethod;
7
+use App\Enums\Accounting\DocumentType;
7
 use App\Enums\Accounting\PaymentMethod;
8
 use App\Enums\Accounting\PaymentMethod;
8
 use App\Filament\Company\Resources\Purchases\BillResource\Pages;
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
 use App\Filament\Tables\Actions\ReplicateBulkAction;
12
 use App\Filament\Tables\Actions\ReplicateBulkAction;
11
 use App\Filament\Tables\Filters\DateRangeFilter;
13
 use App\Filament\Tables\Filters\DateRangeFilter;
12
 use App\Models\Accounting\Adjustment;
14
 use App\Models\Accounting\Adjustment;
13
 use App\Models\Accounting\Bill;
15
 use App\Models\Accounting\Bill;
14
 use App\Models\Banking\BankAccount;
16
 use App\Models\Banking\BankAccount;
15
 use App\Models\Common\Offering;
17
 use App\Models\Common\Offering;
18
+use App\Models\Common\Vendor;
19
+use App\Utilities\Currency\CurrencyAccessor;
16
 use App\Utilities\Currency\CurrencyConverter;
20
 use App\Utilities\Currency\CurrencyConverter;
21
+use App\Utilities\RateCalculator;
17
 use Awcodes\TableRepeater\Components\TableRepeater;
22
 use Awcodes\TableRepeater\Components\TableRepeater;
18
 use Awcodes\TableRepeater\Header;
23
 use Awcodes\TableRepeater\Header;
19
 use Closure;
24
 use Closure;
49
                                     ->relationship('vendor', 'name')
54
                                     ->relationship('vendor', 'name')
50
                                     ->preload()
55
                                     ->preload()
51
                                     ->searchable()
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
                             Forms\Components\Group::make([
72
                             Forms\Components\Group::make([
55
                                 Forms\Components\TextInput::make('bill_number')
73
                                 Forms\Components\TextInput::make('bill_number')
111
                             })
129
                             })
112
                             ->schema([
130
                             ->schema([
113
                                 Forms\Components\Select::make('offering_id')
131
                                 Forms\Components\Select::make('offering_id')
132
+                                    ->label('Item')
114
                                     ->relationship('purchasableOffering', 'name')
133
                                     ->relationship('purchasableOffering', 'name')
115
                                     ->preload()
134
                                     ->preload()
116
                                     ->searchable()
135
                                     ->searchable()
138
                                     ->live()
157
                                     ->live()
139
                                     ->default(1),
158
                                     ->default(1),
140
                                 Forms\Components\TextInput::make('unit_price')
159
                                 Forms\Components\TextInput::make('unit_price')
160
+                                    ->label('Price')
141
                                     ->hiddenLabel()
161
                                     ->hiddenLabel()
142
                                     ->numeric()
162
                                     ->numeric()
143
                                     ->live()
163
                                     ->live()
144
                                     ->default(0),
164
                                     ->default(0),
145
                                 Forms\Components\Select::make('purchaseTaxes')
165
                                 Forms\Components\Select::make('purchaseTaxes')
166
+                                    ->label('Taxes')
146
                                     ->relationship('purchaseTaxes', 'name')
167
                                     ->relationship('purchaseTaxes', 'name')
147
                                     ->saveRelationshipsUsing(null)
168
                                     ->saveRelationshipsUsing(null)
148
                                     ->dehydrated(true)
169
                                     ->dehydrated(true)
151
                                     ->live()
172
                                     ->live()
152
                                     ->searchable(),
173
                                     ->searchable(),
153
                                 Forms\Components\Select::make('purchaseDiscounts')
174
                                 Forms\Components\Select::make('purchaseDiscounts')
175
+                                    ->label('Discounts')
154
                                     ->relationship('purchaseDiscounts', 'name')
176
                                     ->relationship('purchaseDiscounts', 'name')
155
                                     ->saveRelationshipsUsing(null)
177
                                     ->saveRelationshipsUsing(null)
156
                                     ->dehydrated(true)
178
                                     ->dehydrated(true)
165
                                     ->searchable(),
187
                                     ->searchable(),
166
                                 Forms\Components\Placeholder::make('total')
188
                                 Forms\Components\Placeholder::make('total')
167
                                     ->hiddenLabel()
189
                                     ->hiddenLabel()
190
+                                    ->extraAttributes(['class' => 'text-left sm:text-right'])
168
                                     ->content(function (Forms\Get $get) {
191
                                     ->content(function (Forms\Get $get) {
169
                                         $quantity = max((float) ($get('quantity') ?? 0), 0);
192
                                         $quantity = max((float) ($get('quantity') ?? 0), 0);
170
                                         $unitPrice = max((float) ($get('unit_price') ?? 0), 0);
193
                                         $unitPrice = max((float) ($get('unit_price') ?? 0), 0);
171
                                         $purchaseTaxes = $get('purchaseTaxes') ?? [];
194
                                         $purchaseTaxes = $get('purchaseTaxes') ?? [];
172
                                         $purchaseDiscounts = $get('purchaseDiscounts') ?? [];
195
                                         $purchaseDiscounts = $get('purchaseDiscounts') ?? [];
196
+                                        $currencyCode = $get('../../currency_code') ?? CurrencyAccessor::getDefaultCurrency();
173
 
197
 
174
                                         $subtotal = $quantity * $unitPrice;
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
                                         // Final total
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
                 Tables\Columns\TextColumn::make('vendor.name')
258
                 Tables\Columns\TextColumn::make('vendor.name')
226
                     ->sortable(),
259
                     ->sortable(),
227
                 Tables\Columns\TextColumn::make('total')
260
                 Tables\Columns\TextColumn::make('total')
228
-                    ->currency()
229
-                    ->sortable(),
261
+                    ->currencyWithConversion(static fn (Bill $record) => $record->currency_code)
262
+                    ->sortable()
263
+                    ->toggleable(),
230
                 Tables\Columns\TextColumn::make('amount_paid')
264
                 Tables\Columns\TextColumn::make('amount_paid')
231
                     ->label('Amount Paid')
265
                     ->label('Amount Paid')
232
-                    ->currency()
233
-                    ->sortable(),
266
+                    ->currencyWithConversion(static fn (Bill $record) => $record->currency_code)
267
+                    ->sortable()
268
+                    ->toggleable(),
234
                 Tables\Columns\TextColumn::make('amount_due')
269
                 Tables\Columns\TextColumn::make('amount_due')
235
                     ->label('Amount Due')
270
                     ->label('Amount Due')
236
-                    ->currency()
271
+                    ->currencyWithConversion(static fn (Bill $record) => $record->currency_code)
237
                     ->sortable(),
272
                     ->sortable(),
238
             ])
273
             ])
239
             ->filters([
274
             ->filters([
289
                             Forms\Components\TextInput::make('amount')
324
                             Forms\Components\TextInput::make('amount')
290
                                 ->label('Amount')
325
                                 ->label('Amount')
291
                                 ->required()
326
                                 ->required()
292
-                                ->money()
327
+                                ->money(fn (Bill $record) => $record->currency_code)
293
                                 ->live(onBlur: true)
328
                                 ->live(onBlur: true)
294
                                 ->helperText(function (Bill $record, $state) {
329
                                 ->helperText(function (Bill $record, $state) {
295
-                                    if (! CurrencyConverter::isValidAmount($state)) {
330
+                                    $billCurrency = $record->currency_code;
331
+                                    if (! CurrencyConverter::isValidAmount($state, $billCurrency)) {
296
                                         return null;
332
                                         return null;
297
                                     }
333
                                     }
298
 
334
 
299
                                     $amountDue = $record->getRawOriginal('amount_due');
335
                                     $amountDue = $record->getRawOriginal('amount_due');
300
-                                    $amount = CurrencyConverter::convertToCents($state);
336
+                                    $amount = CurrencyConverter::convertToCents($state, $billCurrency);
301
 
337
 
302
                                     if ($amount <= 0) {
338
                                     if ($amount <= 0) {
303
                                         return 'Please enter a valid positive amount';
339
                                         return 'Please enter a valid positive amount';
306
                                     $newAmountDue = $amountDue - $amount;
342
                                     $newAmountDue = $amountDue - $amount;
307
 
343
 
308
                                     return match (true) {
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
                                         $newAmountDue === 0 => 'Bill will be fully paid',
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
                                 ->rules([
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
                                             $fail('Please enter a valid amount');
353
                                             $fail('Please enter a valid amount');
318
                                         }
354
                                         }
319
                                     },
355
                                     },
395
                             if ($cantRecordPayments) {
431
                             if ($cantRecordPayments) {
396
                                 Notification::make()
432
                                 Notification::make()
397
                                     ->title('Payment Recording Failed')
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
                                     ->persistent()
435
                                     ->persistent()
400
                                     ->danger()
436
                                     ->danger()
401
                                     ->send();
437
                                     ->send();

+ 7
- 4
app/Filament/Company/Resources/Purchases/BillResource/Widgets/BillOverview.php 查看文件

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

+ 0
- 1
app/Filament/Company/Resources/Purchases/VendorResource.php 查看文件

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

+ 1
- 2
app/Filament/Company/Resources/Sales/ClientResource.php 查看文件

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

+ 67
- 35
app/Filament/Company/Resources/Sales/InvoiceResource.php 查看文件

2
 
2
 
3
 namespace App\Filament\Company\Resources\Sales;
3
 namespace App\Filament\Company\Resources\Sales;
4
 
4
 
5
-use App\Collections\Accounting\InvoiceCollection;
5
+use App\Collections\Accounting\DocumentCollection;
6
 use App\Enums\Accounting\DocumentDiscountMethod;
6
 use App\Enums\Accounting\DocumentDiscountMethod;
7
+use App\Enums\Accounting\DocumentType;
7
 use App\Enums\Accounting\InvoiceStatus;
8
 use App\Enums\Accounting\InvoiceStatus;
8
 use App\Enums\Accounting\PaymentMethod;
9
 use App\Enums\Accounting\PaymentMethod;
9
 use App\Filament\Company\Resources\Sales\InvoiceResource\Pages;
10
 use App\Filament\Company\Resources\Sales\InvoiceResource\Pages;
10
 use App\Filament\Company\Resources\Sales\InvoiceResource\RelationManagers;
11
 use App\Filament\Company\Resources\Sales\InvoiceResource\RelationManagers;
11
 use App\Filament\Company\Resources\Sales\InvoiceResource\Widgets;
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
 use App\Filament\Tables\Actions\ReplicateBulkAction;
15
 use App\Filament\Tables\Actions\ReplicateBulkAction;
14
 use App\Filament\Tables\Filters\DateRangeFilter;
16
 use App\Filament\Tables\Filters\DateRangeFilter;
15
 use App\Models\Accounting\Adjustment;
17
 use App\Models\Accounting\Adjustment;
16
 use App\Models\Accounting\Invoice;
18
 use App\Models\Accounting\Invoice;
17
 use App\Models\Banking\BankAccount;
19
 use App\Models\Banking\BankAccount;
20
+use App\Models\Common\Client;
18
 use App\Models\Common\Offering;
21
 use App\Models\Common\Offering;
22
+use App\Utilities\Currency\CurrencyAccessor;
19
 use App\Utilities\Currency\CurrencyConverter;
23
 use App\Utilities\Currency\CurrencyConverter;
24
+use App\Utilities\RateCalculator;
20
 use Awcodes\TableRepeater\Components\TableRepeater;
25
 use Awcodes\TableRepeater\Components\TableRepeater;
21
 use Awcodes\TableRepeater\Header;
26
 use Awcodes\TableRepeater\Header;
22
 use Closure;
27
 use Closure;
97
                                     ->relationship('client', 'name')
102
                                     ->relationship('client', 'name')
98
                                     ->preload()
103
                                     ->preload()
99
                                     ->searchable()
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
                             Forms\Components\Group::make([
120
                             Forms\Components\Group::make([
103
                                 Forms\Components\TextInput::make('invoice_number')
121
                                 Forms\Components\TextInput::make('invoice_number')
222
                                     ->searchable(),
240
                                     ->searchable(),
223
                                 Forms\Components\Placeholder::make('total')
241
                                 Forms\Components\Placeholder::make('total')
224
                                     ->hiddenLabel()
242
                                     ->hiddenLabel()
243
+                                    ->extraAttributes(['class' => 'text-left sm:text-right'])
225
                                     ->content(function (Forms\Get $get) {
244
                                     ->content(function (Forms\Get $get) {
226
                                         $quantity = max((float) ($get('quantity') ?? 0), 0);
245
                                         $quantity = max((float) ($get('quantity') ?? 0), 0);
227
                                         $unitPrice = max((float) ($get('unit_price') ?? 0), 0);
246
                                         $unitPrice = max((float) ($get('unit_price') ?? 0), 0);
228
                                         $salesTaxes = $get('salesTaxes') ?? [];
247
                                         $salesTaxes = $get('salesTaxes') ?? [];
229
                                         $salesDiscounts = $get('salesDiscounts') ?? [];
248
                                         $salesDiscounts = $get('salesDiscounts') ?? [];
249
+                                        $currencyCode = $get('../../currency_code') ?? CurrencyAccessor::getDefaultCurrency();
230
 
250
 
231
                                         $subtotal = $quantity * $unitPrice;
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
                                         // Final total
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
                         Forms\Components\Textarea::make('terms')
283
                         Forms\Components\Textarea::make('terms')
255
                             ->columnSpanFull(),
284
                             ->columnSpanFull(),
256
                     ]),
285
                     ]),
291
                     ->sortable()
320
                     ->sortable()
292
                     ->searchable(),
321
                     ->searchable(),
293
                 Tables\Columns\TextColumn::make('total')
322
                 Tables\Columns\TextColumn::make('total')
294
-                    ->currency()
295
-                    ->sortable(),
323
+                    ->currencyWithConversion(static fn (Invoice $record) => $record->currency_code)
324
+                    ->sortable()
325
+                    ->toggleable(),
296
                 Tables\Columns\TextColumn::make('amount_paid')
326
                 Tables\Columns\TextColumn::make('amount_paid')
297
                     ->label('Amount Paid')
327
                     ->label('Amount Paid')
298
-                    ->currency()
299
-                    ->sortable(),
328
+                    ->currencyWithConversion(static fn (Invoice $record) => $record->currency_code)
329
+                    ->sortable()
330
+                    ->toggleable(),
300
                 Tables\Columns\TextColumn::make('amount_due')
331
                 Tables\Columns\TextColumn::make('amount_due')
301
                     ->label('Amount Due')
332
                     ->label('Amount Due')
302
-                    ->currency()
333
+                    ->currencyWithConversion(static fn (Invoice $record) => $record->currency_code)
303
                     ->sortable(),
334
                     ->sortable(),
304
             ])
335
             ])
305
             ->filters([
336
             ->filters([
357
                             Forms\Components\TextInput::make('amount')
388
                             Forms\Components\TextInput::make('amount')
358
                                 ->label('Amount')
389
                                 ->label('Amount')
359
                                 ->required()
390
                                 ->required()
360
-                                ->money()
391
+                                ->money(fn (Invoice $record) => $record->currency_code)
361
                                 ->live(onBlur: true)
392
                                 ->live(onBlur: true)
362
                                 ->helperText(function (Invoice $record, $state) {
393
                                 ->helperText(function (Invoice $record, $state) {
363
-                                    if (! CurrencyConverter::isValidAmount($state)) {
394
+                                    $invoiceCurrency = $record->currency_code;
395
+                                    if (! CurrencyConverter::isValidAmount($state, $invoiceCurrency)) {
364
                                         return null;
396
                                         return null;
365
                                     }
397
                                     }
366
 
398
 
367
                                     $amountDue = $record->getRawOriginal('amount_due');
399
                                     $amountDue = $record->getRawOriginal('amount_due');
368
 
400
 
369
-                                    $amount = CurrencyConverter::convertToCents($state);
401
+                                    $amount = CurrencyConverter::convertToCents($state, $invoiceCurrency);
370
 
402
 
371
                                     if ($amount <= 0) {
403
                                     if ($amount <= 0) {
372
                                         return 'Please enter a valid positive amount';
404
                                         return 'Please enter a valid positive amount';
379
                                     }
411
                                     }
380
 
412
 
381
                                     return match (true) {
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
                                         $newAmountDue === 0 => 'Invoice will be fully paid',
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
                                 ->rules([
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
                                             $fail('Please enter a valid amount');
422
                                             $fail('Please enter a valid amount');
391
                                         }
423
                                         }
392
                                     },
424
                                     },
523
                             if ($cantRecordPayments) {
555
                             if ($cantRecordPayments) {
524
                                 Notification::make()
556
                                 Notification::make()
525
                                     ->title('Payment Recording Failed')
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
                                     ->persistent()
559
                                     ->persistent()
528
                                     ->danger()
560
                                     ->danger()
529
                                     ->send();
561
                                     ->send();
531
                                 $action->cancel(true);
563
                                 $action->cancel(true);
532
                             }
564
                             }
533
                         })
565
                         })
534
-                        ->mountUsing(function (InvoiceCollection $records, Form $form) {
566
+                        ->mountUsing(function (DocumentCollection $records, Form $form) {
535
                             $totalAmountDue = $records->sumMoneyFormattedSimple('amount_due');
567
                             $totalAmountDue = $records->sumMoneyFormattedSimple('amount_due');
536
 
568
 
537
                             $form->fill([
569
                             $form->fill([
567
                             Forms\Components\Textarea::make('notes')
599
                             Forms\Components\Textarea::make('notes')
568
                                 ->label('Notes'),
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
                             $totalPaymentAmount = CurrencyConverter::convertToCents($data['amount']);
603
                             $totalPaymentAmount = CurrencyConverter::convertToCents($data['amount']);
572
                             $totalAmountDue = $records->sumMoneyInCents('amount_due');
604
                             $totalAmountDue = $records->sumMoneyInCents('amount_due');
573
 
605
 
584
                                 $action->halt(true);
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
                             $totalPaymentAmount = CurrencyConverter::convertToCents($data['amount']);
620
                             $totalPaymentAmount = CurrencyConverter::convertToCents($data['amount']);
589
 
621
 
590
                             $remainingAmount = $totalPaymentAmount;
622
                             $remainingAmount = $totalPaymentAmount;

+ 1
- 1
app/Filament/Company/Resources/Sales/InvoiceResource/Pages/ViewInvoice.php 查看文件

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

+ 6
- 4
app/Filament/Company/Resources/Sales/InvoiceResource/Widgets/InvoiceOverview.php 查看文件

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

+ 0
- 37
app/Filament/Forms/Components/BillTotals.php 查看文件

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 查看文件

26
             ->required()
26
             ->required()
27
             ->createOptionForm($this->createCurrencyForm())
27
             ->createOptionForm($this->createCurrencyForm())
28
             ->createOptionAction(fn (Action $action) => $this->createCurrencyAction($action));
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
     protected function createCurrencyForm(): array
45
     protected function createCurrencyForm(): array
56
         return $action
70
         return $action
57
             ->label('Add Currency')
71
             ->label('Add Currency')
58
             ->slideOver()
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 查看文件

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 查看文件

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 查看文件

1
 <?php
1
 <?php
2
 
2
 
3
+use App\Enums\Accounting\AdjustmentComputation;
3
 use App\Enums\Setting\NumberFormat;
4
 use App\Enums\Setting\NumberFormat;
4
 use App\Models\Setting\Localization;
5
 use App\Models\Setting\Localization;
5
 use Filament\Support\RawJs;
6
 use Filament\Support\RawJs;
44
 if (! function_exists('ratePrefix')) {
45
 if (! function_exists('ratePrefix')) {
45
     function ratePrefix($computation, ?string $currency = null): ?string
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
         return null;
59
         return null;
65
 if (! function_exists('rateSuffix')) {
63
 if (! function_exists('rateSuffix')) {
66
     function rateSuffix($computation, ?string $currency = null): ?string
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
         return null;
77
         return null;
84
 }
79
 }
85
 
80
 
86
 if (! function_exists('rateMask')) {
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
             return percentMask(4);
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
             return null;
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
             return $state . '%';
115
             return $state . '%';
122
         }
116
         }
123
 
117
 
124
-        if ($computation === 'fixed') {
118
+        if ($computationEnum->isFixed()) {
125
             return money($state, $currency, true)->formatWithCode();
119
             return money($state, $currency, true)->formatWithCode();
126
         }
120
         }
127
 
121
 

+ 71
- 13
app/Models/Accounting/Bill.php 查看文件

4
 
4
 
5
 use App\Casts\MoneyCast;
5
 use App\Casts\MoneyCast;
6
 use App\Casts\RateCast;
6
 use App\Casts\RateCast;
7
+use App\Collections\Accounting\DocumentCollection;
7
 use App\Concerns\Blamable;
8
 use App\Concerns\Blamable;
8
 use App\Concerns\CompanyOwned;
9
 use App\Concerns\CompanyOwned;
9
 use App\Enums\Accounting\AdjustmentComputation;
10
 use App\Enums\Accounting\AdjustmentComputation;
12
 use App\Enums\Accounting\JournalEntryType;
13
 use App\Enums\Accounting\JournalEntryType;
13
 use App\Enums\Accounting\TransactionType;
14
 use App\Enums\Accounting\TransactionType;
14
 use App\Filament\Company\Resources\Purchases\BillResource;
15
 use App\Filament\Company\Resources\Purchases\BillResource;
16
+use App\Models\Banking\BankAccount;
15
 use App\Models\Common\Vendor;
17
 use App\Models\Common\Vendor;
18
+use App\Models\Setting\Currency;
16
 use App\Observers\BillObserver;
19
 use App\Observers\BillObserver;
20
+use App\Utilities\Currency\CurrencyAccessor;
17
 use App\Utilities\Currency\CurrencyConverter;
21
 use App\Utilities\Currency\CurrencyConverter;
18
 use Filament\Actions\MountableAction;
22
 use Filament\Actions\MountableAction;
19
 use Filament\Actions\ReplicateAction;
23
 use Filament\Actions\ReplicateAction;
24
+use Illuminate\Database\Eloquent\Attributes\CollectedBy;
20
 use Illuminate\Database\Eloquent\Attributes\ObservedBy;
25
 use Illuminate\Database\Eloquent\Attributes\ObservedBy;
21
 use Illuminate\Database\Eloquent\Builder;
26
 use Illuminate\Database\Eloquent\Builder;
22
 use Illuminate\Database\Eloquent\Casts\Attribute;
27
 use Illuminate\Database\Eloquent\Casts\Attribute;
28
 use Illuminate\Support\Carbon;
33
 use Illuminate\Support\Carbon;
29
 
34
 
30
 #[ObservedBy(BillObserver::class)]
35
 #[ObservedBy(BillObserver::class)]
36
+#[CollectedBy(DocumentCollection::class)]
31
 class Bill extends Model
37
 class Bill extends Model
32
 {
38
 {
33
     use Blamable;
39
     use Blamable;
75
         'amount_due' => MoneyCast::class,
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
     public function vendor(): BelongsTo
89
     public function vendor(): BelongsTo
79
     {
90
     {
80
         return $this->belongsTo(Vendor::class);
91
         return $this->belongsTo(Vendor::class);
128
         return ! in_array($this->status, [
139
         return ! in_array($this->status, [
129
             BillStatus::Paid,
140
             BillStatus::Paid,
130
             BillStatus::Void,
141
             BillStatus::Void,
131
-        ]);
142
+        ]) && $this->currency_code === CurrencyAccessor::getDefaultCurrency();
132
     }
143
     }
133
 
144
 
134
     public function hasPayments(): bool
145
     public function hasPayments(): bool
188
         $transactionType = TransactionType::Withdrawal;
199
         $transactionType = TransactionType::Withdrawal;
189
         $transactionDescription = "Bill #{$this->bill_number}: Payment to {$this->vendor->name}";
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
         $this->transactions()->create([
225
         $this->transactions()->create([
193
             'company_id' => $this->company_id,
226
             'company_id' => $this->company_id,
194
             'type' => $transactionType,
227
             'type' => $transactionType,
195
             'is_payment' => true,
228
             'is_payment' => true,
196
             'posted_at' => $data['posted_at'],
229
             'posted_at' => $data['posted_at'],
197
-            'amount' => $data['amount'],
230
+            'amount' => $formattedAmountForBankCurrency,
198
             'payment_method' => $data['payment_method'],
231
             'payment_method' => $data['payment_method'],
199
             'bank_account_id' => $data['bank_account_id'],
232
             'bank_account_id' => $data['bank_account_id'],
200
             'account_id' => Account::getAccountsPayableAccount()->id,
233
             'account_id' => Account::getAccountsPayableAccount()->id,
207
     {
240
     {
208
         $postedAt ??= $this->date;
241
         $postedAt ??= $this->date;
209
 
242
 
243
+        $total = $this->formatAmountToDefaultCurrency($this->getRawOriginal('total'));
244
+
210
         $transaction = $this->transactions()->create([
245
         $transaction = $this->transactions()->create([
211
             'company_id' => $this->company_id,
246
             'company_id' => $this->company_id,
212
             'type' => TransactionType::Journal,
247
             'type' => TransactionType::Journal,
213
             'posted_at' => $postedAt,
248
             'posted_at' => $postedAt,
214
-            'amount' => $this->total,
249
+            'amount' => $total,
215
             'description' => 'Bill Creation for Bill #' . $this->bill_number,
250
             'description' => 'Bill Creation for Bill #' . $this->bill_number,
216
         ]);
251
         ]);
217
 
252
 
221
             'company_id' => $this->company_id,
256
             'company_id' => $this->company_id,
222
             'type' => JournalEntryType::Credit,
257
             'type' => JournalEntryType::Credit,
223
             'account_id' => Account::getAccountsPayableAccount()->id,
258
             'account_id' => Account::getAccountsPayableAccount()->id,
224
-            'amount' => $this->total,
259
+            'amount' => $total,
225
             'description' => $baseDescription,
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
         $remainingDiscountCents = $billDiscountTotalCents;
265
         $remainingDiscountCents = $billDiscountTotalCents;
231
 
266
 
232
         foreach ($this->lineItems as $index => $lineItem) {
267
         foreach ($this->lineItems as $index => $lineItem) {
233
             $lineItemDescription = "{$baseDescription} › {$lineItem->offering->name}";
268
             $lineItemDescription = "{$baseDescription} › {$lineItem->offering->name}";
234
 
269
 
270
+            $lineItemSubtotal = $this->formatAmountToDefaultCurrency($lineItem->getRawOriginal('subtotal'));
271
+
235
             $transaction->journalEntries()->create([
272
             $transaction->journalEntries()->create([
236
                 'company_id' => $this->company_id,
273
                 'company_id' => $this->company_id,
237
                 'type' => JournalEntryType::Debit,
274
                 'type' => JournalEntryType::Debit,
238
                 'account_id' => $lineItem->offering->expense_account_id,
275
                 'account_id' => $lineItem->offering->expense_account_id,
239
-                'amount' => $lineItem->subtotal,
276
+                'amount' => $lineItemSubtotal,
240
                 'description' => $lineItemDescription,
277
                 'description' => $lineItemDescription,
241
             ]);
278
             ]);
242
 
279
 
243
             foreach ($lineItem->adjustments as $adjustment) {
280
             foreach ($lineItem->adjustments as $adjustment) {
281
+                $adjustmentAmount = $this->formatAmountToDefaultCurrency($lineItem->calculateAdjustmentTotalAmount($adjustment));
282
+
244
                 if ($adjustment->isNonRecoverablePurchaseTax()) {
283
                 if ($adjustment->isNonRecoverablePurchaseTax()) {
245
                     $transaction->journalEntries()->create([
284
                     $transaction->journalEntries()->create([
246
                         'company_id' => $this->company_id,
285
                         'company_id' => $this->company_id,
247
                         'type' => JournalEntryType::Debit,
286
                         'type' => JournalEntryType::Debit,
248
                         'account_id' => $lineItem->offering->expense_account_id,
287
                         'account_id' => $lineItem->offering->expense_account_id,
249
-                        'amount' => $lineItem->calculateAdjustmentTotal($adjustment)->getAmount(),
288
+                        'amount' => $adjustmentAmount,
250
                         'description' => "{$lineItemDescription} ({$adjustment->name})",
289
                         'description' => "{$lineItemDescription} ({$adjustment->name})",
251
                     ]);
290
                     ]);
252
                 } elseif ($adjustment->account_id) {
291
                 } elseif ($adjustment->account_id) {
254
                         'company_id' => $this->company_id,
293
                         'company_id' => $this->company_id,
255
                         'type' => $adjustment->category->isDiscount() ? JournalEntryType::Credit : JournalEntryType::Debit,
294
                         'type' => $adjustment->category->isDiscount() ? JournalEntryType::Credit : JournalEntryType::Debit,
256
                         'account_id' => $adjustment->account_id,
295
                         'account_id' => $adjustment->account_id,
257
-                        'amount' => $lineItem->calculateAdjustmentTotal($adjustment)->getAmount(),
296
+                        'amount' => $adjustmentAmount,
258
                         'description' => $lineItemDescription,
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
                 if ($index === $this->lineItems->count() - 1) {
305
                 if ($index === $this->lineItems->count() - 1) {
267
                     $lineItemDiscount = $remainingDiscountCents;
306
                     $lineItemDiscount = $remainingDiscountCents;
268
                 } else {
307
                 } else {
269
                     $lineItemDiscount = (int) round(
308
                     $lineItemDiscount = (int) round(
270
-                        ($lineItemSubtotalCents / $totalLineItemSubtotal) * $billDiscountTotalCents
309
+                        ($lineItemSubtotalCents / $totalLineItemSubtotalCents) * $billDiscountTotalCents
271
                     );
310
                     );
272
                     $remainingDiscountCents -= $lineItemDiscount;
311
                     $remainingDiscountCents -= $lineItemDiscount;
273
                 }
312
                 }
296
         $this->createInitialTransaction();
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
     public static function getReplicateAction(string $action = ReplicateAction::class): MountableAction
357
     public static function getReplicateAction(string $action = ReplicateAction::class): MountableAction
300
     {
358
     {
301
         return $action::make()
359
         return $action::make()

+ 44
- 0
app/Models/Accounting/DocumentLineItem.php 查看文件

12
 use App\Models\Common\Offering;
12
 use App\Models\Common\Offering;
13
 use App\Observers\DocumentLineItemObserver;
13
 use App\Observers\DocumentLineItemObserver;
14
 use App\Utilities\Currency\CurrencyAccessor;
14
 use App\Utilities\Currency\CurrencyAccessor;
15
+use App\Utilities\RateCalculator;
15
 use Illuminate\Database\Eloquent\Attributes\ObservedBy;
16
 use Illuminate\Database\Eloquent\Attributes\ObservedBy;
16
 use Illuminate\Database\Eloquent\Factories\HasFactory;
17
 use Illuminate\Database\Eloquent\Factories\HasFactory;
17
 use Illuminate\Database\Eloquent\Model;
18
 use Illuminate\Database\Eloquent\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
     public function calculateDiscountTotal(): Money
132
     public function calculateDiscountTotal(): Money
117
     {
133
     {
118
         $subtotal = money($this->subtotal, CurrencyAccessor::getDefaultCurrency());
134
         $subtotal = money($this->subtotal, CurrencyAccessor::getDefaultCurrency());
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
     public function calculateAdjustmentTotal(Adjustment $adjustment): Money
157
     public function calculateAdjustmentTotal(Adjustment $adjustment): Money
127
     {
158
     {
128
         $subtotal = money($this->subtotal, CurrencyAccessor::getDefaultCurrency());
159
         $subtotal = money($this->subtotal, CurrencyAccessor::getDefaultCurrency());
129
 
160
 
130
         return $subtotal->multiply($adjustment->rate / 100);
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 查看文件

4
 
4
 
5
 use App\Casts\MoneyCast;
5
 use App\Casts\MoneyCast;
6
 use App\Casts\RateCast;
6
 use App\Casts\RateCast;
7
-use App\Collections\Accounting\InvoiceCollection;
7
+use App\Collections\Accounting\DocumentCollection;
8
 use App\Concerns\Blamable;
8
 use App\Concerns\Blamable;
9
 use App\Concerns\CompanyOwned;
9
 use App\Concerns\CompanyOwned;
10
 use App\Enums\Accounting\AdjustmentComputation;
10
 use App\Enums\Accounting\AdjustmentComputation;
13
 use App\Enums\Accounting\JournalEntryType;
13
 use App\Enums\Accounting\JournalEntryType;
14
 use App\Enums\Accounting\TransactionType;
14
 use App\Enums\Accounting\TransactionType;
15
 use App\Filament\Company\Resources\Sales\InvoiceResource;
15
 use App\Filament\Company\Resources\Sales\InvoiceResource;
16
+use App\Models\Banking\BankAccount;
16
 use App\Models\Common\Client;
17
 use App\Models\Common\Client;
18
+use App\Models\Setting\Currency;
17
 use App\Observers\InvoiceObserver;
19
 use App\Observers\InvoiceObserver;
20
+use App\Utilities\Currency\CurrencyAccessor;
18
 use App\Utilities\Currency\CurrencyConverter;
21
 use App\Utilities\Currency\CurrencyConverter;
19
 use Filament\Actions\Action;
22
 use Filament\Actions\Action;
20
 use Filament\Actions\MountableAction;
23
 use Filament\Actions\MountableAction;
31
 use Illuminate\Support\Carbon;
34
 use Illuminate\Support\Carbon;
32
 
35
 
33
 #[ObservedBy(InvoiceObserver::class)]
36
 #[ObservedBy(InvoiceObserver::class)]
34
-#[CollectedBy(InvoiceCollection::class)]
37
+#[CollectedBy(DocumentCollection::class)]
35
 class Invoice extends Model
38
 class Invoice extends Model
36
 {
39
 {
37
     use Blamable;
40
     use Blamable;
92
         return $this->belongsTo(Client::class);
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
     public function lineItems(): MorphMany
103
     public function lineItems(): MorphMany
96
     {
104
     {
97
         return $this->morphMany(DocumentLineItem::class, 'documentable');
105
         return $this->morphMany(DocumentLineItem::class, 'documentable');
161
             InvoiceStatus::Paid,
169
             InvoiceStatus::Paid,
162
             InvoiceStatus::Void,
170
             InvoiceStatus::Void,
163
             InvoiceStatus::Overpaid,
171
             InvoiceStatus::Overpaid,
164
-        ]);
172
+        ]) && $this->currency_code === CurrencyAccessor::getDefaultCurrency();
165
     }
173
     }
166
 
174
 
167
     public function canBeOverdue(): bool
175
     public function canBeOverdue(): bool
219
             $transactionDescription = "Invoice #{$this->invoice_number}: Payment from {$this->client->name}";
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
         // Create transaction
251
         // Create transaction
223
         $this->transactions()->create([
252
         $this->transactions()->create([
224
             'company_id' => $this->company_id,
253
             'company_id' => $this->company_id,
225
             'type' => $transactionType,
254
             'type' => $transactionType,
226
             'is_payment' => true,
255
             'is_payment' => true,
227
             'posted_at' => $data['posted_at'],
256
             'posted_at' => $data['posted_at'],
228
-            'amount' => $data['amount'],
257
+            'amount' => $formattedAmountForBankCurrency,
229
             'payment_method' => $data['payment_method'],
258
             'payment_method' => $data['payment_method'],
230
             'bank_account_id' => $data['bank_account_id'],
259
             'bank_account_id' => $data['bank_account_id'],
231
             'account_id' => Account::getAccountsReceivableAccount()->id,
260
             'account_id' => Account::getAccountsReceivableAccount()->id,
252
 
281
 
253
     public function createApprovalTransaction(): void
282
     public function createApprovalTransaction(): void
254
     {
283
     {
284
+        $total = $this->formatAmountToDefaultCurrency($this->getRawOriginal('total'));
285
+
255
         $transaction = $this->transactions()->create([
286
         $transaction = $this->transactions()->create([
256
             'company_id' => $this->company_id,
287
             'company_id' => $this->company_id,
257
             'type' => TransactionType::Journal,
288
             'type' => TransactionType::Journal,
258
             'posted_at' => $this->date,
289
             'posted_at' => $this->date,
259
-            'amount' => $this->total,
290
+            'amount' => $total,
260
             'description' => 'Invoice Approval for Invoice #' . $this->invoice_number,
291
             'description' => 'Invoice Approval for Invoice #' . $this->invoice_number,
261
         ]);
292
         ]);
262
 
293
 
266
             'company_id' => $this->company_id,
297
             'company_id' => $this->company_id,
267
             'type' => JournalEntryType::Debit,
298
             'type' => JournalEntryType::Debit,
268
             'account_id' => Account::getAccountsReceivableAccount()->id,
299
             'account_id' => Account::getAccountsReceivableAccount()->id,
269
-            'amount' => $this->total,
300
+            'amount' => $total,
270
             'description' => $baseDescription,
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
         $remainingDiscountCents = $invoiceDiscountTotalCents;
306
         $remainingDiscountCents = $invoiceDiscountTotalCents;
276
 
307
 
277
         foreach ($this->lineItems as $index => $lineItem) {
308
         foreach ($this->lineItems as $index => $lineItem) {
278
             $lineItemDescription = "{$baseDescription} › {$lineItem->offering->name}";
309
             $lineItemDescription = "{$baseDescription} › {$lineItem->offering->name}";
279
 
310
 
311
+            $lineItemSubtotal = $this->formatAmountToDefaultCurrency($lineItem->getRawOriginal('subtotal'));
312
+
280
             $transaction->journalEntries()->create([
313
             $transaction->journalEntries()->create([
281
                 'company_id' => $this->company_id,
314
                 'company_id' => $this->company_id,
282
                 'type' => JournalEntryType::Credit,
315
                 'type' => JournalEntryType::Credit,
283
                 'account_id' => $lineItem->offering->income_account_id,
316
                 'account_id' => $lineItem->offering->income_account_id,
284
-                'amount' => $lineItem->subtotal,
317
+                'amount' => $lineItemSubtotal,
285
                 'description' => $lineItemDescription,
318
                 'description' => $lineItemDescription,
286
             ]);
319
             ]);
287
 
320
 
288
             foreach ($lineItem->adjustments as $adjustment) {
321
             foreach ($lineItem->adjustments as $adjustment) {
322
+                $adjustmentAmount = $this->formatAmountToDefaultCurrency($lineItem->calculateAdjustmentTotalAmount($adjustment));
323
+
289
                 $transaction->journalEntries()->create([
324
                 $transaction->journalEntries()->create([
290
                     'company_id' => $this->company_id,
325
                     'company_id' => $this->company_id,
291
                     'type' => $adjustment->category->isDiscount() ? JournalEntryType::Debit : JournalEntryType::Credit,
326
                     'type' => $adjustment->category->isDiscount() ? JournalEntryType::Debit : JournalEntryType::Credit,
292
                     'account_id' => $adjustment->account_id,
327
                     'account_id' => $adjustment->account_id,
293
-                    'amount' => $lineItem->calculateAdjustmentTotal($adjustment)->getAmount(),
328
+                    'amount' => $adjustmentAmount,
294
                     'description' => $lineItemDescription,
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
                 if ($index === $this->lineItems->count() - 1) {
336
                 if ($index === $this->lineItems->count() - 1) {
302
                     $lineItemDiscount = $remainingDiscountCents;
337
                     $lineItemDiscount = $remainingDiscountCents;
303
                 } else {
338
                 } else {
304
                     $lineItemDiscount = (int) round(
339
                     $lineItemDiscount = (int) round(
305
-                        ($lineItemSubtotalCents / $totalLineItemSubtotal) * $invoiceDiscountTotalCents
340
+                        ($lineItemSubtotalCents / $totalLineItemSubtotalCents) * $invoiceDiscountTotalCents
306
                     );
341
                     );
307
                     $remainingDiscountCents -= $lineItemDiscount;
342
                     $remainingDiscountCents -= $lineItemDiscount;
308
                 }
343
                 }
331
         $this->createApprovalTransaction();
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
     public static function getApproveDraftAction(string $action = Action::class): MountableAction
388
     public static function getApproveDraftAction(string $action = Action::class): MountableAction
335
     {
389
     {
336
         return $action::make('approveDraft')
390
         return $action::make('approveDraft')

+ 38
- 15
app/Observers/TransactionObserver.php 查看文件

107
             return;
107
             return;
108
         }
108
         }
109
 
109
 
110
-        $depositTotal = (int) $invoice->deposits()
110
+        $invoiceCurrency = $invoice->currency_code;
111
+
112
+        $depositTotalInInvoiceCurrencyCents = (int) $invoice->deposits()
111
             ->when($excludedTransaction, fn (Builder $query) => $query->whereKeyNot($excludedTransaction->getKey()))
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
             ->when($excludedTransaction, fn (Builder $query) => $query->whereKeyNot($excludedTransaction->getKey()))
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
         $newStatus = match (true) {
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
             default => InvoiceStatus::Partial,
139
             default => InvoiceStatus::Partial,
126
         };
140
         };
127
 
141
 
134
         }
148
         }
135
 
149
 
136
         $invoice->update([
150
         $invoice->update([
137
-            'amount_paid' => CurrencyConverter::convertCentsToFloat($totalPaid),
151
+            'amount_paid' => CurrencyConverter::convertCentsToFormatSimple($totalPaidInInvoiceCurrencyCents, $invoiceCurrency),
138
             'status' => $newStatus,
152
             'status' => $newStatus,
139
             'paid_at' => $paidAt,
153
             'paid_at' => $paidAt,
140
         ]);
154
         ]);
146
             return;
160
             return;
147
         }
161
         }
148
 
162
 
149
-        $withdrawalTotal = (int) $bill->withdrawals()
163
+        $billCurrency = $bill->currency_code;
164
+
165
+        $withdrawalTotalInBillCurrencyCents = (int) $bill->withdrawals()
150
             ->when($excludedTransaction, fn (Builder $query) => $query->whereKeyNot($excludedTransaction->getKey()))
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
         $newStatus = match (true) {
179
         $newStatus = match (true) {
157
-            $totalPaid >= $billTotal => BillStatus::Paid,
180
+            $totalPaidInBillCurrencyCents >= $billTotalInBillCurrencyCents => BillStatus::Paid,
158
             default => BillStatus::Partial,
181
             default => BillStatus::Partial,
159
         };
182
         };
160
 
183
 
167
         }
190
         }
168
 
191
 
169
         $bill->update([
192
         $bill->update([
170
-            'amount_paid' => CurrencyConverter::convertCentsToFloat($totalPaid),
193
+            'amount_paid' => CurrencyConverter::convertCentsToFormatSimple($totalPaidInBillCurrencyCents, $billCurrency),
171
             'status' => $newStatus,
194
             'status' => $newStatus,
172
             'paid_at' => $paidAt,
195
             'paid_at' => $paidAt,
173
         ]);
196
         ]);

+ 130
- 34
app/Providers/MacroServiceProvider.php 查看文件

4
 
4
 
5
 use Akaunting\Money\Currency;
5
 use Akaunting\Money\Currency;
6
 use Akaunting\Money\Money;
6
 use Akaunting\Money\Money;
7
+use App\Enums\Accounting\AdjustmentComputation;
7
 use App\Enums\Setting\DateFormat;
8
 use App\Enums\Setting\DateFormat;
8
 use App\Models\Accounting\AccountSubtype;
9
 use App\Models\Accounting\AccountSubtype;
9
 use App\Models\Setting\Localization;
10
 use App\Models\Setting\Localization;
10
 use App\Utilities\Accounting\AccountCode;
11
 use App\Utilities\Accounting\AccountCode;
11
 use App\Utilities\Currency\CurrencyAccessor;
12
 use App\Utilities\Currency\CurrencyAccessor;
13
+use App\Utilities\Currency\CurrencyConverter;
12
 use BackedEnum;
14
 use BackedEnum;
13
 use Carbon\CarbonInterface;
15
 use Carbon\CarbonInterface;
14
 use Closure;
16
 use Closure;
61
             return $this;
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
         TextColumn::macro('defaultDateFormat', function (): static {
116
         TextColumn::macro('defaultDateFormat', function (): static {
65
             $localization = Localization::firstOrFail();
117
             $localization = Localization::firstOrFail();
66
 
118
 
102
             return $this;
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
             return $this;
241
             return $this;
146
         });
242
         });

+ 53
- 0
app/Utilities/RateCalculator.php 查看文件

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 查看文件

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 查看文件

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 查看文件

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 查看文件

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

+ 85
- 85
composer.lock 查看文件

4
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
4
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
5
         "This file is @generated automatically"
5
         "This file is @generated automatically"
6
     ],
6
     ],
7
-    "content-hash": "7301f370a98e2573adf4e6b4e7ab4a8e",
7
+    "content-hash": "a04479d57fe01a2694ad92a8901363ea",
8
     "packages": [
8
     "packages": [
9
         {
9
         {
10
             "name": "akaunting/laravel-money",
10
             "name": "akaunting/laravel-money",
497
         },
497
         },
498
         {
498
         {
499
             "name": "aws/aws-sdk-php",
499
             "name": "aws/aws-sdk-php",
500
-            "version": "3.334.6",
500
+            "version": "3.336.0",
501
             "source": {
501
             "source": {
502
                 "type": "git",
502
                 "type": "git",
503
                 "url": "https://github.com/aws/aws-sdk-php.git",
503
                 "url": "https://github.com/aws/aws-sdk-php.git",
504
-                "reference": "2b0be3aded849d3b7bb0b53ea3295c7cecdeeee7"
504
+                "reference": "5a62003bf183e32da038056a4b9077c27224e034"
505
             },
505
             },
506
             "dist": {
506
             "dist": {
507
                 "type": "zip",
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
                 "shasum": ""
510
                 "shasum": ""
511
             },
511
             },
512
             "require": {
512
             "require": {
589
             "support": {
589
             "support": {
590
                 "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
590
                 "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
591
                 "issues": "https://github.com/aws/aws-sdk-php/issues",
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
             "name": "aws/aws-sdk-php-laravel",
597
             "name": "aws/aws-sdk-php-laravel",
1662
         },
1662
         },
1663
         {
1663
         {
1664
             "name": "filament/actions",
1664
             "name": "filament/actions",
1665
-            "version": "v3.2.129",
1665
+            "version": "v3.2.131",
1666
             "source": {
1666
             "source": {
1667
                 "type": "git",
1667
                 "type": "git",
1668
                 "url": "https://github.com/filamentphp/actions.git",
1668
                 "url": "https://github.com/filamentphp/actions.git",
1669
-                "reference": "1ee8b0a890b53e8b0b341134d3ba9bdaeee294d3"
1669
+                "reference": "8d9ceaf392eeff55fd335f5169d14f84af8c325e"
1670
             },
1670
             },
1671
             "dist": {
1671
             "dist": {
1672
                 "type": "zip",
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
                 "shasum": ""
1675
                 "shasum": ""
1676
             },
1676
             },
1677
             "require": {
1677
             "require": {
1711
                 "issues": "https://github.com/filamentphp/filament/issues",
1711
                 "issues": "https://github.com/filamentphp/filament/issues",
1712
                 "source": "https://github.com/filamentphp/filament"
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
             "name": "filament/filament",
1717
             "name": "filament/filament",
1718
-            "version": "v3.2.129",
1718
+            "version": "v3.2.131",
1719
             "source": {
1719
             "source": {
1720
                 "type": "git",
1720
                 "type": "git",
1721
                 "url": "https://github.com/filamentphp/panels.git",
1721
                 "url": "https://github.com/filamentphp/panels.git",
1722
-                "reference": "9a327a54dec24e0b12c437ef799828f492cb882a"
1722
+                "reference": "21febddcc6720b250b41386805a8dbd1deef2c56"
1723
             },
1723
             },
1724
             "dist": {
1724
             "dist": {
1725
                 "type": "zip",
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
                 "shasum": ""
1728
                 "shasum": ""
1729
             },
1729
             },
1730
             "require": {
1730
             "require": {
1776
                 "issues": "https://github.com/filamentphp/filament/issues",
1776
                 "issues": "https://github.com/filamentphp/filament/issues",
1777
                 "source": "https://github.com/filamentphp/filament"
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
             "name": "filament/forms",
1782
             "name": "filament/forms",
1783
-            "version": "v3.2.129",
1783
+            "version": "v3.2.131",
1784
             "source": {
1784
             "source": {
1785
                 "type": "git",
1785
                 "type": "git",
1786
                 "url": "https://github.com/filamentphp/forms.git",
1786
                 "url": "https://github.com/filamentphp/forms.git",
1787
-                "reference": "e6133bdc03de05dfe23eac386b49e51093338883"
1787
+                "reference": "72429b0df9c3d123273dd51ba62b764e2114697c"
1788
             },
1788
             },
1789
             "dist": {
1789
             "dist": {
1790
                 "type": "zip",
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
                 "shasum": ""
1793
                 "shasum": ""
1794
             },
1794
             },
1795
             "require": {
1795
             "require": {
1832
                 "issues": "https://github.com/filamentphp/filament/issues",
1832
                 "issues": "https://github.com/filamentphp/filament/issues",
1833
                 "source": "https://github.com/filamentphp/filament"
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
             "name": "filament/infolists",
1838
             "name": "filament/infolists",
1839
-            "version": "v3.2.129",
1839
+            "version": "v3.2.131",
1840
             "source": {
1840
             "source": {
1841
                 "type": "git",
1841
                 "type": "git",
1842
                 "url": "https://github.com/filamentphp/infolists.git",
1842
                 "url": "https://github.com/filamentphp/infolists.git",
1843
-                "reference": "0db994330e7fe21a9684256ea1fc34fee916a8d6"
1843
+                "reference": "15c200a3172b88a6247ff4b7230f69982d848194"
1844
             },
1844
             },
1845
             "dist": {
1845
             "dist": {
1846
                 "type": "zip",
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
                 "shasum": ""
1849
                 "shasum": ""
1850
             },
1850
             },
1851
             "require": {
1851
             "require": {
1883
                 "issues": "https://github.com/filamentphp/filament/issues",
1883
                 "issues": "https://github.com/filamentphp/filament/issues",
1884
                 "source": "https://github.com/filamentphp/filament"
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
             "name": "filament/notifications",
1889
             "name": "filament/notifications",
1890
-            "version": "v3.2.129",
1890
+            "version": "v3.2.131",
1891
             "source": {
1891
             "source": {
1892
                 "type": "git",
1892
                 "type": "git",
1893
                 "url": "https://github.com/filamentphp/notifications.git",
1893
                 "url": "https://github.com/filamentphp/notifications.git",
1939
         },
1939
         },
1940
         {
1940
         {
1941
             "name": "filament/support",
1941
             "name": "filament/support",
1942
-            "version": "v3.2.129",
1942
+            "version": "v3.2.131",
1943
             "source": {
1943
             "source": {
1944
                 "type": "git",
1944
                 "type": "git",
1945
                 "url": "https://github.com/filamentphp/support.git",
1945
                 "url": "https://github.com/filamentphp/support.git",
1946
-                "reference": "e8aed9684d5c58ff7dde9517c7f1af44d575d871"
1946
+                "reference": "ddc16d8da50d73f7300671b70c9dcb942d845d9d"
1947
             },
1947
             },
1948
             "dist": {
1948
             "dist": {
1949
                 "type": "zip",
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
                 "shasum": ""
1952
                 "shasum": ""
1953
             },
1953
             },
1954
             "require": {
1954
             "require": {
1994
                 "issues": "https://github.com/filamentphp/filament/issues",
1994
                 "issues": "https://github.com/filamentphp/filament/issues",
1995
                 "source": "https://github.com/filamentphp/filament"
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
             "name": "filament/tables",
2000
             "name": "filament/tables",
2001
-            "version": "v3.2.129",
2001
+            "version": "v3.2.131",
2002
             "source": {
2002
             "source": {
2003
                 "type": "git",
2003
                 "type": "git",
2004
                 "url": "https://github.com/filamentphp/tables.git",
2004
                 "url": "https://github.com/filamentphp/tables.git",
2005
-                "reference": "acdec73ae82961654a52a22ed9f53207cfee2ef8"
2005
+                "reference": "224aea12a4a4cfcd158b53df94cdd190f8226cac"
2006
             },
2006
             },
2007
             "dist": {
2007
             "dist": {
2008
                 "type": "zip",
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
                 "shasum": ""
2011
                 "shasum": ""
2012
             },
2012
             },
2013
             "require": {
2013
             "require": {
2046
                 "issues": "https://github.com/filamentphp/filament/issues",
2046
                 "issues": "https://github.com/filamentphp/filament/issues",
2047
                 "source": "https://github.com/filamentphp/filament"
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
             "name": "filament/widgets",
2052
             "name": "filament/widgets",
2053
-            "version": "v3.2.129",
2053
+            "version": "v3.2.131",
2054
             "source": {
2054
             "source": {
2055
                 "type": "git",
2055
                 "type": "git",
2056
                 "url": "https://github.com/filamentphp/widgets.git",
2056
                 "url": "https://github.com/filamentphp/widgets.git",
2057
-                "reference": "6de1c84d71168fd1c6a5b1ae1e1b4ec5ee4b6f55"
2057
+                "reference": "869a419fe42e2cf1b9461f2d1e702e2fcad030ae"
2058
             },
2058
             },
2059
             "dist": {
2059
             "dist": {
2060
                 "type": "zip",
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
                 "shasum": ""
2063
                 "shasum": ""
2064
             },
2064
             },
2065
             "require": {
2065
             "require": {
2090
                 "issues": "https://github.com/filamentphp/filament/issues",
2090
                 "issues": "https://github.com/filamentphp/filament/issues",
2091
                 "source": "https://github.com/filamentphp/filament"
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
             "name": "firebase/php-jwt",
2096
             "name": "firebase/php-jwt",
2980
         },
2980
         },
2981
         {
2981
         {
2982
             "name": "laravel/framework",
2982
             "name": "laravel/framework",
2983
-            "version": "v11.35.1",
2983
+            "version": "v11.36.1",
2984
             "source": {
2984
             "source": {
2985
                 "type": "git",
2985
                 "type": "git",
2986
                 "url": "https://github.com/laravel/framework.git",
2986
                 "url": "https://github.com/laravel/framework.git",
2987
-                "reference": "dcfa130ede1a6fa4343dc113410963e791ad34fb"
2987
+                "reference": "df06f5163f4550641fdf349ebc04916a61135a64"
2988
             },
2988
             },
2989
             "dist": {
2989
             "dist": {
2990
                 "type": "zip",
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
                 "shasum": ""
2993
                 "shasum": ""
2994
             },
2994
             },
2995
             "require": {
2995
             "require": {
3010
                 "guzzlehttp/uri-template": "^1.0",
3010
                 "guzzlehttp/uri-template": "^1.0",
3011
                 "laravel/prompts": "^0.1.18|^0.2.0|^0.3.0",
3011
                 "laravel/prompts": "^0.1.18|^0.2.0|^0.3.0",
3012
                 "laravel/serializable-closure": "^1.3|^2.0",
3012
                 "laravel/serializable-closure": "^1.3|^2.0",
3013
-                "league/commonmark": "^2.2.1",
3013
+                "league/commonmark": "^2.6",
3014
                 "league/flysystem": "^3.25.1",
3014
                 "league/flysystem": "^3.25.1",
3015
                 "league/flysystem-local": "^3.25.1",
3015
                 "league/flysystem-local": "^3.25.1",
3016
                 "league/uri": "^7.5.1",
3016
                 "league/uri": "^7.5.1",
3025
                 "symfony/console": "^7.0.3",
3025
                 "symfony/console": "^7.0.3",
3026
                 "symfony/error-handler": "^7.0.3",
3026
                 "symfony/error-handler": "^7.0.3",
3027
                 "symfony/finder": "^7.0.3",
3027
                 "symfony/finder": "^7.0.3",
3028
-                "symfony/http-foundation": "^7.0.3",
3028
+                "symfony/http-foundation": "^7.2.0",
3029
                 "symfony/http-kernel": "^7.0.3",
3029
                 "symfony/http-kernel": "^7.0.3",
3030
                 "symfony/mailer": "^7.0.3",
3030
                 "symfony/mailer": "^7.0.3",
3031
                 "symfony/mime": "^7.0.3",
3031
                 "symfony/mime": "^7.0.3",
3191
                 "issues": "https://github.com/laravel/framework/issues",
3191
                 "issues": "https://github.com/laravel/framework/issues",
3192
                 "source": "https://github.com/laravel/framework"
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
             "name": "laravel/prompts",
3197
             "name": "laravel/prompts",
3254
         },
3254
         },
3255
         {
3255
         {
3256
             "name": "laravel/sanctum",
3256
             "name": "laravel/sanctum",
3257
-            "version": "v4.0.6",
3257
+            "version": "v4.0.7",
3258
             "source": {
3258
             "source": {
3259
                 "type": "git",
3259
                 "type": "git",
3260
                 "url": "https://github.com/laravel/sanctum.git",
3260
                 "url": "https://github.com/laravel/sanctum.git",
3261
-                "reference": "9e069e36d90b1e1f41886efa0fe9800a6b354694"
3261
+                "reference": "698064236a46df016e64a7eb059b1414e0b281df"
3262
             },
3262
             },
3263
             "dist": {
3263
             "dist": {
3264
                 "type": "zip",
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
                 "shasum": ""
3267
                 "shasum": ""
3268
             },
3268
             },
3269
             "require": {
3269
             "require": {
3314
                 "issues": "https://github.com/laravel/sanctum/issues",
3314
                 "issues": "https://github.com/laravel/sanctum/issues",
3315
                 "source": "https://github.com/laravel/sanctum"
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
             "name": "laravel/serializable-closure",
3320
             "name": "laravel/serializable-closure",
3321
-            "version": "v2.0.0",
3321
+            "version": "v2.0.1",
3322
             "source": {
3322
             "source": {
3323
                 "type": "git",
3323
                 "type": "git",
3324
                 "url": "https://github.com/laravel/serializable-closure.git",
3324
                 "url": "https://github.com/laravel/serializable-closure.git",
3325
-                "reference": "0d8d3d8086984996df86596a86dea60398093a81"
3325
+                "reference": "613b2d4998f85564d40497e05e89cb6d9bd1cbe8"
3326
             },
3326
             },
3327
             "dist": {
3327
             "dist": {
3328
                 "type": "zip",
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
                 "shasum": ""
3331
                 "shasum": ""
3332
             },
3332
             },
3333
             "require": {
3333
             "require": {
3375
                 "issues": "https://github.com/laravel/serializable-closure/issues",
3375
                 "issues": "https://github.com/laravel/serializable-closure/issues",
3376
                 "source": "https://github.com/laravel/serializable-closure"
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
             "name": "laravel/socialite",
3381
             "name": "laravel/socialite",
3382
-            "version": "v5.16.0",
3382
+            "version": "v5.16.1",
3383
             "source": {
3383
             "source": {
3384
                 "type": "git",
3384
                 "type": "git",
3385
                 "url": "https://github.com/laravel/socialite.git",
3385
                 "url": "https://github.com/laravel/socialite.git",
3386
-                "reference": "40a2dc98c53d9dc6d55eadb0d490d3d72b73f1bf"
3386
+                "reference": "4e5be83c0b3ecf81b2ffa47092e917d1f79dce71"
3387
             },
3387
             },
3388
             "dist": {
3388
             "dist": {
3389
                 "type": "zip",
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
                 "shasum": ""
3392
                 "shasum": ""
3393
             },
3393
             },
3394
             "require": {
3394
             "require": {
3398
                 "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0",
3398
                 "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0",
3399
                 "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0",
3399
                 "illuminate/http": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0",
3400
                 "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0",
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
                 "php": "^7.2|^8.0",
3402
                 "php": "^7.2|^8.0",
3403
                 "phpseclib/phpseclib": "^3.0"
3403
                 "phpseclib/phpseclib": "^3.0"
3404
             },
3404
             },
3447
                 "issues": "https://github.com/laravel/socialite/issues",
3447
                 "issues": "https://github.com/laravel/socialite/issues",
3448
                 "source": "https://github.com/laravel/socialite"
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
             "name": "laravel/tinker",
3453
             "name": "laravel/tinker",
3706
         },
3706
         },
3707
         {
3707
         {
3708
             "name": "league/csv",
3708
             "name": "league/csv",
3709
-            "version": "9.20.0",
3709
+            "version": "9.20.1",
3710
             "source": {
3710
             "source": {
3711
                 "type": "git",
3711
                 "type": "git",
3712
                 "url": "https://github.com/thephpleague/csv.git",
3712
                 "url": "https://github.com/thephpleague/csv.git",
3713
-                "reference": "553579df208641ada6ffb450b3a151e2fcfa4f31"
3713
+                "reference": "491d1e79e973a7370c7571dc0fe4a7241f4936ee"
3714
             },
3714
             },
3715
             "dist": {
3715
             "dist": {
3716
                 "type": "zip",
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
                 "shasum": ""
3719
                 "shasum": ""
3720
             },
3720
             },
3721
             "require": {
3721
             "require": {
3789
                     "type": "github"
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
             "name": "league/flysystem",
3795
             "name": "league/flysystem",
4374
         },
4374
         },
4375
         {
4375
         {
4376
             "name": "matomo/device-detector",
4376
             "name": "matomo/device-detector",
4377
-            "version": "6.4.1",
4377
+            "version": "6.4.2",
4378
             "source": {
4378
             "source": {
4379
                 "type": "git",
4379
                 "type": "git",
4380
                 "url": "https://github.com/matomo-org/device-detector.git",
4380
                 "url": "https://github.com/matomo-org/device-detector.git",
4381
-                "reference": "0d364e0dd6c177da3c24cd4049178026324fd7ac"
4381
+                "reference": "806e52d214b05ddead1a1d4304c7592f61f95976"
4382
             },
4382
             },
4383
             "dist": {
4383
             "dist": {
4384
                 "type": "zip",
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
                 "shasum": ""
4387
                 "shasum": ""
4388
             },
4388
             },
4389
             "require": {
4389
             "require": {
4439
                 "source": "https://github.com/matomo-org/matomo",
4439
                 "source": "https://github.com/matomo-org/matomo",
4440
                 "wiki": "https://dev.matomo.org/"
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
             "name": "monolog/monolog",
4445
             "name": "monolog/monolog",
5065
         },
5065
         },
5066
         {
5066
         {
5067
             "name": "openspout/openspout",
5067
             "name": "openspout/openspout",
5068
-            "version": "v4.28.2",
5068
+            "version": "v4.28.3",
5069
             "source": {
5069
             "source": {
5070
                 "type": "git",
5070
                 "type": "git",
5071
                 "url": "https://github.com/openspout/openspout.git",
5071
                 "url": "https://github.com/openspout/openspout.git",
5072
-                "reference": "d6dd654b5db502f28c5773edfa785b516745a142"
5072
+                "reference": "12b5eddcc230a97a9a67a722ad75c247e1a16750"
5073
             },
5073
             },
5074
             "dist": {
5074
             "dist": {
5075
                 "type": "zip",
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
                 "shasum": ""
5078
                 "shasum": ""
5079
             },
5079
             },
5080
             "require": {
5080
             "require": {
5094
                 "phpstan/phpstan": "^2.0.3",
5094
                 "phpstan/phpstan": "^2.0.3",
5095
                 "phpstan/phpstan-phpunit": "^2.0.1",
5095
                 "phpstan/phpstan-phpunit": "^2.0.1",
5096
                 "phpstan/phpstan-strict-rules": "^2",
5096
                 "phpstan/phpstan-strict-rules": "^2",
5097
-                "phpunit/phpunit": "^11.4.4"
5097
+                "phpunit/phpunit": "^11.5.0"
5098
             },
5098
             },
5099
             "suggest": {
5099
             "suggest": {
5100
                 "ext-iconv": "To handle non UTF-8 CSV files (if \"php-mbstring\" is not already installed or is too limited)",
5100
                 "ext-iconv": "To handle non UTF-8 CSV files (if \"php-mbstring\" is not already installed or is too limited)",
5142
             ],
5142
             ],
5143
             "support": {
5143
             "support": {
5144
                 "issues": "https://github.com/openspout/openspout/issues",
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
             "funding": [
5147
             "funding": [
5148
                 {
5148
                 {
5154
                     "type": "github"
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
             "name": "paragonie/constant_time_encoding",
5160
             "name": "paragonie/constant_time_encoding",
12817
             "type": "library",
12817
             "type": "library",
12818
             "extra": {
12818
             "extra": {
12819
                 "thanks": {
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
             "autoload": {
12824
             "autoload": {

+ 1
- 1
config/money.php 查看文件

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

+ 12
- 12
package-lock.json 查看文件

1057
             }
1057
             }
1058
         },
1058
         },
1059
         "node_modules/caniuse-lite": {
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
             "dev": true,
1063
             "dev": true,
1064
             "funding": [
1064
             "funding": [
1065
                 {
1065
                 {
1218
             "license": "MIT"
1218
             "license": "MIT"
1219
         },
1219
         },
1220
         "node_modules/electron-to-chromium": {
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
             "dev": true,
1224
             "dev": true,
1225
             "license": "ISC"
1225
             "license": "ISC"
1226
         },
1226
         },
1569
             }
1569
             }
1570
         },
1570
         },
1571
         "node_modules/jiti": {
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
             "dev": true,
1575
             "dev": true,
1576
             "license": "MIT",
1576
             "license": "MIT",
1577
             "bin": {
1577
             "bin": {
2470
             }
2470
             }
2471
         },
2471
         },
2472
         "node_modules/tailwindcss": {
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
             "dev": true,
2476
             "dev": true,
2477
             "license": "MIT",
2477
             "license": "MIT",
2478
             "dependencies": {
2478
             "dependencies": {

+ 7
- 7
resources/views/filament/company/resources/sales/invoices/components/invoice-view.blade.php 查看文件

113
                         @endif
113
                         @endif
114
                     </td>
114
                     </td>
115
                     <td class="text-center py-3">{{ $item->quantity }}</td>
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
                 </tr>
118
                 </tr>
119
             @endforeach
119
             @endforeach
120
             </tbody>
120
             </tbody>
122
             <tr>
122
             <tr>
123
                 <td class="pl-6 py-2" colspan="2"></td>
123
                 <td class="pl-6 py-2" colspan="2"></td>
124
                 <td class="text-right font-semibold py-2">Subtotal:</td>
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
             </tr>
126
             </tr>
127
             @if($invoice->discount_total)
127
             @if($invoice->discount_total)
128
                 <tr class="text-success-800 dark:text-success-600">
128
                 <tr class="text-success-800 dark:text-success-600">
129
                     <td class="pl-6 py-2" colspan="2"></td>
129
                     <td class="pl-6 py-2" colspan="2"></td>
130
                     <td class="text-right py-2">Discount:</td>
130
                     <td class="text-right py-2">Discount:</td>
131
                     <td class="text-right pr-6 py-2">
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
                     </td>
133
                     </td>
134
                 </tr>
134
                 </tr>
135
             @endif
135
             @endif
137
                 <tr>
137
                 <tr>
138
                     <td class="pl-6 py-2" colspan="2"></td>
138
                     <td class="pl-6 py-2" colspan="2"></td>
139
                     <td class="text-right py-2">Tax:</td>
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
                 </tr>
141
                 </tr>
142
             @endif
142
             @endif
143
             <tr>
143
             <tr>
144
                 <td class="pl-6 py-2" colspan="2"></td>
144
                 <td class="pl-6 py-2" colspan="2"></td>
145
                 <td class="text-right font-semibold border-t py-2">Total:</td>
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
             </tr>
147
             </tr>
148
             <tr>
148
             <tr>
149
                 <td class="pl-6 py-2" colspan="2"></td>
149
                 <td class="pl-6 py-2" colspan="2"></td>
150
                 <td class="text-right font-semibold border-t-4 border-double py-2">Amount Due
150
                 <td class="text-right font-semibold border-t-4 border-double py-2">Amount Due
151
                     ({{ $invoice->currency_code }}):
151
                     ({{ $invoice->currency_code }}):
152
                 </td>
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
             </tr>
154
             </tr>
155
             </tfoot>
155
             </tfoot>
156
         </table>
156
         </table>

+ 0
- 61
resources/views/filament/forms/components/bill-totals.blade.php 查看文件

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 查看文件

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 查看文件

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>

正在加载...
取消
保存