Andrew Wallo 9 个月前
父节点
当前提交
8896f7115c

+ 37
- 0
app/Concerns/GeneratesDocumentNumbers.php 查看文件

@@ -0,0 +1,37 @@
1
+<?php
2
+
3
+namespace App\Concerns;
4
+
5
+use RuntimeException;
6
+
7
+trait GeneratesDocumentNumbers
8
+{
9
+    public static function getNextDocumentNumber(): string
10
+    {
11
+        $company = auth()->user()->currentCompany;
12
+
13
+        if (! $company) {
14
+            throw new RuntimeException('No current company is set for the user.');
15
+        }
16
+
17
+        $settings = $company->{static::getSettingsKey()};
18
+        $numberField = static::getDocumentNumberField();
19
+
20
+        $latestDocument = static::query()
21
+            ->whereNotNull($numberField)
22
+            ->latest($numberField)
23
+            ->first();
24
+
25
+        $lastNumberPart = $latestDocument
26
+            ? (int) substr($latestDocument->{$numberField}, strlen($settings->number_prefix))
27
+            : 0;
28
+
29
+        return $settings->getNumberNext(
30
+            padded: true,
31
+            format: true,
32
+            prefix: $settings->number_prefix,
33
+            digits: $settings->number_digits,
34
+            next: $lastNumberPart + 1
35
+        );
36
+    }
37
+}

+ 32
- 0
app/Concerns/HandlesCurrencyConversion.php 查看文件

@@ -0,0 +1,32 @@
1
+<?php
2
+
3
+namespace App\Concerns;
4
+
5
+use App\Utilities\Currency\CurrencyAccessor;
6
+use App\Utilities\Currency\CurrencyConverter;
7
+
8
+trait HandlesCurrencyConversion
9
+{
10
+    public function convertAmountToDefaultCurrency(int $amountCents): int
11
+    {
12
+        $defaultCurrency = CurrencyAccessor::getDefaultCurrency();
13
+        $needsConversion = $this->currency_code !== $defaultCurrency;
14
+
15
+        if ($needsConversion) {
16
+            return CurrencyConverter::convertBalance(
17
+                $amountCents,
18
+                $this->currency_code,
19
+                $defaultCurrency
20
+            );
21
+        }
22
+
23
+        return $amountCents;
24
+    }
25
+
26
+    public function formatAmountToDefaultCurrency(int $amountCents): string
27
+    {
28
+        $convertedCents = $this->convertAmountToDefaultCurrency($amountCents);
29
+
30
+        return CurrencyConverter::convertCentsToFormatSimple($convertedCents);
31
+    }
32
+}

+ 37
- 0
app/Enums/Accounting/EstimateStatus.php 查看文件

@@ -0,0 +1,37 @@
1
+<?php
2
+
3
+namespace App\Enums\Accounting;
4
+
5
+use App\Enums\Concerns\ParsesEnum;
6
+use Filament\Support\Contracts\HasColor;
7
+use Filament\Support\Contracts\HasLabel;
8
+
9
+enum EstimateStatus: string implements HasColor, HasLabel
10
+{
11
+    use ParsesEnum;
12
+
13
+    case Draft = 'draft';
14
+    case Sent = 'sent';
15
+    case Approved = 'approved';
16
+    case Accepted = 'accepted';
17
+    case Rejected = 'rejected';
18
+    case Expired = 'expired';
19
+    case Converted = 'converted';
20
+
21
+    public function getLabel(): ?string
22
+    {
23
+        return $this->name;
24
+    }
25
+
26
+    public function getColor(): string | array | null
27
+    {
28
+        return match ($this) {
29
+            self::Draft => 'gray',
30
+            self::Sent => 'primary',
31
+            self::Approved, self::Accepted => 'success',
32
+            self::Rejected => 'danger',
33
+            self::Expired => 'warning',
34
+            self::Converted => 'info',
35
+        };
36
+    }
37
+}

+ 27
- 173
app/Models/Accounting/Bill.php 查看文件

@@ -2,118 +2,66 @@
2 2
 
3 3
 namespace App\Models\Accounting;
4 4
 
5
-use App\Casts\MoneyCast;
6
-use App\Casts\RateCast;
7
-use App\Collections\Accounting\DocumentCollection;
8
-use App\Concerns\Blamable;
9
-use App\Concerns\CompanyOwned;
10
-use App\Enums\Accounting\AdjustmentComputation;
11 5
 use App\Enums\Accounting\BillStatus;
12
-use App\Enums\Accounting\DocumentDiscountMethod;
6
+use App\Enums\Accounting\DocumentType;
13 7
 use App\Enums\Accounting\JournalEntryType;
14 8
 use App\Enums\Accounting\TransactionType;
15 9
 use App\Filament\Company\Resources\Purchases\BillResource;
16
-use App\Models\Banking\BankAccount;
17 10
 use App\Models\Common\Vendor;
18
-use App\Models\Setting\Currency;
11
+use App\Models\Setting\DocumentDefault;
19 12
 use App\Observers\BillObserver;
20 13
 use App\Utilities\Currency\CurrencyAccessor;
21 14
 use App\Utilities\Currency\CurrencyConverter;
22 15
 use Filament\Actions\MountableAction;
23 16
 use Filament\Actions\ReplicateAction;
24
-use Illuminate\Database\Eloquent\Attributes\CollectedBy;
25 17
 use Illuminate\Database\Eloquent\Attributes\ObservedBy;
26 18
 use Illuminate\Database\Eloquent\Builder;
27
-use Illuminate\Database\Eloquent\Casts\Attribute;
28
-use Illuminate\Database\Eloquent\Factories\HasFactory;
29
-use Illuminate\Database\Eloquent\Model;
30 19
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
31
-use Illuminate\Database\Eloquent\Relations\MorphMany;
32 20
 use Illuminate\Database\Eloquent\Relations\MorphOne;
33 21
 use Illuminate\Support\Carbon;
34 22
 
35 23
 #[ObservedBy(BillObserver::class)]
36
-#[CollectedBy(DocumentCollection::class)]
37
-class Bill extends Model
24
+class Bill extends Document
38 25
 {
39
-    use Blamable;
40
-    use CompanyOwned;
41
-    use HasFactory;
42
-
43 26
     protected $table = 'bills';
44 27
 
45 28
     protected $fillable = [
46
-        'company_id',
29
+        ...self::COMMON_FILLABLE,
30
+        ...self::BILL_FILLABLE,
31
+    ];
32
+
33
+    protected const BILL_FILLABLE = [
47 34
         'vendor_id',
48 35
         'bill_number',
49
-        'order_number',
50
-        'date',
51
-        'due_date',
52
-        'paid_at',
53
-        'status',
54
-        'currency_code',
55
-        'discount_method',
56
-        'discount_computation',
57
-        'discount_rate',
58
-        'subtotal',
59
-        'tax_total',
60
-        'discount_total',
61
-        'total',
62
-        'amount_paid',
63 36
         'notes',
64
-        'created_by',
65
-        'updated_by',
66 37
     ];
67 38
 
68
-    protected $casts = [
69
-        'date' => 'date',
70
-        'due_date' => 'date',
71
-        'paid_at' => 'datetime',
72
-        'status' => BillStatus::class,
73
-        'discount_method' => DocumentDiscountMethod::class,
74
-        'discount_computation' => AdjustmentComputation::class,
75
-        'discount_rate' => RateCast::class,
76
-        'subtotal' => MoneyCast::class,
77
-        'tax_total' => MoneyCast::class,
78
-        'discount_total' => MoneyCast::class,
79
-        'total' => MoneyCast::class,
80
-        'amount_paid' => MoneyCast::class,
81
-        'amount_due' => MoneyCast::class,
82
-    ];
83
-
84
-    public function currency(): BelongsTo
85
-    {
86
-        return $this->belongsTo(Currency::class, 'currency_code', 'code');
87
-    }
88
-
89
-    public function vendor(): BelongsTo
90
-    {
91
-        return $this->belongsTo(Vendor::class);
92
-    }
93
-
94
-    public function lineItems(): MorphMany
39
+    protected function casts(): array
95 40
     {
96
-        return $this->morphMany(DocumentLineItem::class, 'documentable');
41
+        return [
42
+            ...parent::casts(),
43
+            'status' => BillStatus::class,
44
+        ];
97 45
     }
98 46
 
99
-    public function transactions(): MorphMany
47
+    public static function documentNumberColumn(): string
100 48
     {
101
-        return $this->morphMany(Transaction::class, 'transactionable');
49
+        return 'bill_number';
102 50
     }
103 51
 
104
-    public function payments(): MorphMany
52
+    public static function documentType(): DocumentType
105 53
     {
106
-        return $this->transactions()->where('is_payment', true);
54
+        return DocumentType::Bill;
107 55
     }
108 56
 
109
-    public function deposits(): MorphMany
57
+    public static function getDocumentSettings(): DocumentDefault
110 58
     {
111
-        return $this->transactions()->where('type', TransactionType::Deposit)->where('is_payment', true);
59
+        return auth()->user()->currentCompany->defaultBill;
112 60
     }
113 61
 
114
-    public function withdrawals(): MorphMany
62
+    public function vendor(): BelongsTo
115 63
     {
116
-        return $this->transactions()->where('type', TransactionType::Withdrawal)->where('is_payment', true);
64
+        return $this->belongsTo(Vendor::class);
117 65
     }
118 66
 
119 67
     public function initialTransaction(): MorphOne
@@ -122,13 +70,6 @@ class Bill extends Model
122 70
             ->where('type', TransactionType::Journal);
123 71
     }
124 72
 
125
-    protected function isCurrentlyOverdue(): Attribute
126
-    {
127
-        return Attribute::get(function () {
128
-            return $this->due_date->isBefore(today()) && $this->canBeOverdue();
129
-        });
130
-    }
131
-
132 73
     public function canBeOverdue(): bool
133 74
     {
134 75
         return in_array($this->status, BillStatus::canBeOverdue());
@@ -142,44 +83,6 @@ class Bill extends Model
142 83
         ]) && $this->currency_code === CurrencyAccessor::getDefaultCurrency();
143 84
     }
144 85
 
145
-    public function hasPayments(): bool
146
-    {
147
-        return $this->payments->isNotEmpty();
148
-    }
149
-
150
-    public static function getNextDocumentNumber(): string
151
-    {
152
-        $company = auth()->user()->currentCompany;
153
-
154
-        if (! $company) {
155
-            throw new \RuntimeException('No current company is set for the user.');
156
-        }
157
-
158
-        $defaultBillSettings = $company->defaultBill;
159
-
160
-        $numberPrefix = $defaultBillSettings->number_prefix;
161
-        $numberDigits = $defaultBillSettings->number_digits;
162
-
163
-        $latestDocument = static::query()
164
-            ->whereNotNull('bill_number')
165
-            ->latest('bill_number')
166
-            ->first();
167
-
168
-        $lastNumberNumericPart = $latestDocument
169
-            ? (int) substr($latestDocument->bill_number, strlen($numberPrefix))
170
-            : 0;
171
-
172
-        $numberNext = $lastNumberNumericPart + 1;
173
-
174
-        return $defaultBillSettings->getNumberNext(
175
-            padded: true,
176
-            format: true,
177
-            prefix: $numberPrefix,
178
-            digits: $numberDigits,
179
-            next: $numberNext
180
-        );
181
-    }
182
-
183 86
     public function hasInitialTransaction(): bool
184 87
     {
185 88
         return $this->initialTransaction()->exists();
@@ -196,44 +99,14 @@ class Bill extends Model
196 99
 
197 100
     public function recordPayment(array $data): void
198 101
     {
199
-        $transactionType = TransactionType::Withdrawal;
200 102
         $transactionDescription = "Bill #{$this->bill_number}: Payment to {$this->vendor->name}";
201 103
 
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
225
-        $this->transactions()->create([
226
-            'company_id' => $this->company_id,
227
-            'type' => $transactionType,
228
-            'is_payment' => true,
229
-            'posted_at' => $data['posted_at'],
230
-            'amount' => $formattedAmountForBankCurrency,
231
-            'payment_method' => $data['payment_method'],
232
-            'bank_account_id' => $data['bank_account_id'],
233
-            'account_id' => Account::getAccountsPayableAccount()->id,
234
-            'description' => $transactionDescription,
235
-            'notes' => $data['notes'] ?? null,
236
-        ]);
104
+        $this->recordTransaction(
105
+            $data,
106
+            TransactionType::Withdrawal, // Always withdrawal for bills
107
+            $transactionDescription,
108
+            Account::getAccountsPayableAccount()->id // Account ID specific to bills
109
+        );
237 110
     }
238 111
 
239 112
     public function createInitialTransaction(?Carbon $postedAt = null): void
@@ -335,25 +208,6 @@ class Bill extends Model
335 208
         $this->createInitialTransaction();
336 209
     }
337 210
 
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
-
357 211
     public static function getReplicateAction(string $action = ReplicateAction::class): MountableAction
358 212
     {
359 213
         return $action::make()

+ 223
- 0
app/Models/Accounting/Document.php 查看文件

@@ -0,0 +1,223 @@
1
+<?php
2
+
3
+namespace App\Models\Accounting;
4
+
5
+use App\Casts\MoneyCast;
6
+use App\Casts\RateCast;
7
+use App\Collections\Accounting\DocumentCollection;
8
+use App\Concerns\Blamable;
9
+use App\Concerns\CompanyOwned;
10
+use App\Enums\Accounting\AdjustmentComputation;
11
+use App\Enums\Accounting\DocumentDiscountMethod;
12
+use App\Enums\Accounting\DocumentType;
13
+use App\Enums\Accounting\TransactionType;
14
+use App\Models\Banking\BankAccount;
15
+use App\Models\Setting\Currency;
16
+use App\Models\Setting\DocumentDefault;
17
+use App\Utilities\Currency\CurrencyAccessor;
18
+use App\Utilities\Currency\CurrencyConverter;
19
+use Illuminate\Database\Eloquent\Casts\Attribute;
20
+use Illuminate\Database\Eloquent\Collection;
21
+use Illuminate\Database\Eloquent\Factories\HasFactory;
22
+use Illuminate\Database\Eloquent\Model;
23
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
24
+use Illuminate\Database\Eloquent\Relations\MorphMany;
25
+
26
+abstract class Document extends Model
27
+{
28
+    use Blamable;
29
+    use CompanyOwned;
30
+    use HasFactory;
31
+
32
+    protected const COMMON_FILLABLE = [
33
+        'company_id',
34
+        'order_number',
35
+        'date',
36
+        'due_date',
37
+        'paid_at',
38
+        'status',
39
+        'currency_code',
40
+        'discount_method',
41
+        'discount_computation',
42
+        'discount_rate',
43
+        'subtotal',
44
+        'tax_total',
45
+        'discount_total',
46
+        'total',
47
+        'amount_paid',
48
+        'created_by',
49
+        'updated_by',
50
+    ];
51
+
52
+    protected $fillable = self::COMMON_FILLABLE;
53
+
54
+    protected function casts(): array
55
+    {
56
+        return [
57
+            'date' => 'date',
58
+            'due_date' => 'date',
59
+            'paid_at' => 'datetime',
60
+            'discount_method' => DocumentDiscountMethod::class,
61
+            'discount_computation' => AdjustmentComputation::class,
62
+            'discount_rate' => RateCast::class,
63
+            'subtotal' => MoneyCast::class,
64
+            'tax_total' => MoneyCast::class,
65
+            'discount_total' => MoneyCast::class,
66
+            'total' => MoneyCast::class,
67
+            'amount_paid' => MoneyCast::class,
68
+            'amount_due' => MoneyCast::class,
69
+        ];
70
+    }
71
+
72
+    public function currency(): BelongsTo
73
+    {
74
+        return $this->belongsTo(Currency::class, 'currency_code', 'code');
75
+    }
76
+
77
+    public function lineItems(): MorphMany
78
+    {
79
+        return $this->morphMany(DocumentLineItem::class, 'documentable');
80
+    }
81
+
82
+    public function transactions(): MorphMany
83
+    {
84
+        return $this->morphMany(Transaction::class, 'transactionable');
85
+    }
86
+
87
+    public function payments(): MorphMany
88
+    {
89
+        return $this->transactions()->where('is_payment', true);
90
+    }
91
+
92
+    public function deposits(): MorphMany
93
+    {
94
+        return $this->transactions()->where('type', TransactionType::Deposit)->where('is_payment', true);
95
+    }
96
+
97
+    public function withdrawals(): MorphMany
98
+    {
99
+        return $this->transactions()->where('type', TransactionType::Withdrawal)->where('is_payment', true);
100
+    }
101
+
102
+    public function hasPayments(): bool
103
+    {
104
+        return $this->payments->isNotEmpty();
105
+    }
106
+
107
+    protected function isCurrentlyOverdue(): Attribute
108
+    {
109
+        return Attribute::get(function () {
110
+            return $this->due_date->isBefore(today()) && $this->canBeOverdue();
111
+        });
112
+    }
113
+
114
+    public static function getNextDocumentNumber(): string
115
+    {
116
+        $company = auth()->user()->currentCompany;
117
+
118
+        if (! $company) {
119
+            throw new \RuntimeException('No current company is set for the user.');
120
+        }
121
+
122
+        $settings = static::getDocumentSettings();
123
+
124
+        $prefix = $settings->number_prefix;
125
+        $digits = $settings->number_digits;
126
+
127
+        $latestDocument = static::query()
128
+            ->whereNotNull(static::documentNumberColumn())
129
+            ->latest(static::documentNumberColumn())
130
+            ->first();
131
+
132
+        $lastNumber = $latestDocument
133
+            ? (int) substr($latestDocument->{static::documentNumberColumn()}, strlen($prefix))
134
+            : 0;
135
+
136
+        $numberNext = $lastNumber + 1;
137
+
138
+        return $settings->getNumberNext(
139
+            padded: true,
140
+            format: true,
141
+            prefix: $prefix,
142
+            digits: $digits,
143
+            next: $numberNext
144
+        );
145
+    }
146
+
147
+    protected function recordTransaction(array $data, string $transactionType, string $transactionDescription, int $accountId): void
148
+    {
149
+        $formattedAmount = $this->prepareAmountForTransaction($data);
150
+
151
+        $this->transactions()->create([
152
+            'company_id' => $this->company_id,
153
+            'type' => $transactionType,
154
+            'is_payment' => true,
155
+            'posted_at' => $data['posted_at'],
156
+            'amount' => $formattedAmount,
157
+            'payment_method' => $data['payment_method'],
158
+            'bank_account_id' => $data['bank_account_id'],
159
+            'account_id' => $accountId,
160
+            'description' => $transactionDescription,
161
+            'notes' => $data['notes'] ?? null,
162
+        ]);
163
+    }
164
+
165
+    protected function prepareAmountForTransaction(array $data): string
166
+    {
167
+        $bankAccount = BankAccount::findOrFail($data['bank_account_id']);
168
+        $bankAccountCurrency = $bankAccount->account->currency_code ?? CurrencyAccessor::getDefaultCurrency();
169
+
170
+        $requiresConversion = $this->currency_code !== $bankAccountCurrency;
171
+
172
+        if ($requiresConversion) {
173
+            $amountInCents = CurrencyConverter::convertToCents($data['amount'], $this->currency_code);
174
+            $convertedAmount = CurrencyConverter::convertBalance(
175
+                $amountInCents,
176
+                $this->currency_code,
177
+                $bankAccountCurrency
178
+            );
179
+
180
+            return CurrencyConverter::convertCentsToFormatSimple($convertedAmount, $bankAccountCurrency);
181
+        }
182
+
183
+        return $data['amount'];
184
+    }
185
+
186
+    public function convertAmountToDefaultCurrency(int $amountCents): int
187
+    {
188
+        $defaultCurrency = CurrencyAccessor::getDefaultCurrency();
189
+        $needsConversion = $this->currency_code !== $defaultCurrency;
190
+
191
+        if ($needsConversion) {
192
+            return CurrencyConverter::convertBalance($amountCents, $this->currency_code, $defaultCurrency);
193
+        }
194
+
195
+        return $amountCents;
196
+    }
197
+
198
+    public function formatAmountToDefaultCurrency(int $amountCents): string
199
+    {
200
+        $convertedCents = $this->convertAmountToDefaultCurrency($amountCents);
201
+
202
+        return CurrencyConverter::convertCentsToFormatSimple($convertedCents);
203
+    }
204
+
205
+    /**
206
+     * Create a new Eloquent Collection instance.
207
+     *
208
+     * @param  array<int, Model>  $models
209
+     * @return Collection<int, Model>
210
+     */
211
+    public function newCollection(array $models = []): Collection
212
+    {
213
+        return new DocumentCollection($models);
214
+    }
215
+
216
+    abstract public static function documentType(): DocumentType;
217
+
218
+    abstract public static function documentNumberColumn(): string;
219
+
220
+    abstract public static function getDocumentSettings(): DocumentDefault;
221
+
222
+    abstract public function canBeOverdue(): bool;
223
+}

+ 61
- 0
app/Models/Accounting/Estimate.php 查看文件

@@ -0,0 +1,61 @@
1
+<?php
2
+
3
+namespace App\Models\Accounting;
4
+
5
+use App\Casts\MoneyCast;
6
+use App\Casts\RateCast;
7
+use App\Concerns\Blamable;
8
+use App\Concerns\CompanyOwned;
9
+use App\Enums\Accounting\AdjustmentComputation;
10
+use App\Enums\Accounting\DocumentDiscountMethod;
11
+use App\Enums\Accounting\EstimateStatus;
12
+use Illuminate\Database\Eloquent\Factories\HasFactory;
13
+use Illuminate\Database\Eloquent\Model;
14
+
15
+class Estimate extends Model
16
+{
17
+    use Blamable;
18
+    use CompanyOwned;
19
+    use HasFactory;
20
+
21
+    protected $fillable = [
22
+        'client_id',
23
+        'logo',
24
+        'header',
25
+        'subheader',
26
+        'number',
27
+        'reference_number',
28
+        'date',
29
+        'expiry_date',
30
+        'approved_at',
31
+        'accepted_at',
32
+        'last_sent',
33
+        'status',
34
+        'currency_code',
35
+        'discount_method',
36
+        'discount_computation',
37
+        'discount_rate',
38
+        'subtotal',
39
+        'tax_total',
40
+        'discount_total',
41
+        'total',
42
+        'terms',
43
+        'footer',
44
+    ];
45
+
46
+    protected $casts = [
47
+        'date' => 'date',
48
+        'expiry_date' => 'date',
49
+        'approved_at' => 'datetime',
50
+        'accepted_at' => 'datetime',
51
+        'last_sent' => 'datetime',
52
+        'status' => EstimateStatus::class,
53
+        'discount_method' => DocumentDiscountMethod::class,
54
+        'discount_computation' => AdjustmentComputation::class,
55
+        'discount_rate' => RateCast::class,
56
+        'subtotal' => MoneyCast::class,
57
+        'tax_total' => MoneyCast::class,
58
+        'discount_total' => MoneyCast::class,
59
+        'total' => MoneyCast::class,
60
+    ];
61
+}

+ 35
- 179
app/Models/Accounting/Invoice.php 查看文件

@@ -2,127 +2,75 @@
2 2
 
3 3
 namespace App\Models\Accounting;
4 4
 
5
-use App\Casts\MoneyCast;
6
-use App\Casts\RateCast;
7
-use App\Collections\Accounting\DocumentCollection;
8
-use App\Concerns\Blamable;
9
-use App\Concerns\CompanyOwned;
10
-use App\Enums\Accounting\AdjustmentComputation;
11
-use App\Enums\Accounting\DocumentDiscountMethod;
5
+use App\Enums\Accounting\DocumentType;
12 6
 use App\Enums\Accounting\InvoiceStatus;
13 7
 use App\Enums\Accounting\JournalEntryType;
14 8
 use App\Enums\Accounting\TransactionType;
15 9
 use App\Filament\Company\Resources\Sales\InvoiceResource;
16
-use App\Models\Banking\BankAccount;
17 10
 use App\Models\Common\Client;
18
-use App\Models\Setting\Currency;
11
+use App\Models\Setting\DocumentDefault;
19 12
 use App\Observers\InvoiceObserver;
20 13
 use App\Utilities\Currency\CurrencyAccessor;
21 14
 use App\Utilities\Currency\CurrencyConverter;
22 15
 use Filament\Actions\Action;
23 16
 use Filament\Actions\MountableAction;
24 17
 use Filament\Actions\ReplicateAction;
25
-use Illuminate\Database\Eloquent\Attributes\CollectedBy;
26 18
 use Illuminate\Database\Eloquent\Attributes\ObservedBy;
27 19
 use Illuminate\Database\Eloquent\Builder;
28
-use Illuminate\Database\Eloquent\Casts\Attribute;
29
-use Illuminate\Database\Eloquent\Factories\HasFactory;
30
-use Illuminate\Database\Eloquent\Model;
31 20
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
32
-use Illuminate\Database\Eloquent\Relations\MorphMany;
33 21
 use Illuminate\Database\Eloquent\Relations\MorphOne;
34 22
 use Illuminate\Support\Carbon;
35 23
 
36 24
 #[ObservedBy(InvoiceObserver::class)]
37
-#[CollectedBy(DocumentCollection::class)]
38
-class Invoice extends Model
25
+class Invoice extends Document
39 26
 {
40
-    use Blamable;
41
-    use CompanyOwned;
42
-    use HasFactory;
43
-
44 27
     protected $table = 'invoices';
45 28
 
46 29
     protected $fillable = [
47
-        'company_id',
30
+        ...self::COMMON_FILLABLE,
31
+        ...self::INVOICE_FILLABLE,
32
+    ];
33
+
34
+    protected const INVOICE_FILLABLE = [
48 35
         'client_id',
49 36
         'logo',
50 37
         'header',
51 38
         'subheader',
52 39
         'invoice_number',
53
-        'order_number',
54
-        'date',
55
-        'due_date',
56 40
         'approved_at',
57
-        'paid_at',
58 41
         'last_sent',
59
-        'status',
60
-        'currency_code',
61
-        'discount_method',
62
-        'discount_computation',
63
-        'discount_rate',
64
-        'subtotal',
65
-        'tax_total',
66
-        'discount_total',
67
-        'total',
68
-        'amount_paid',
69 42
         'terms',
70 43
         'footer',
71
-        'created_by',
72
-        'updated_by',
73 44
     ];
74 45
 
75
-    protected $casts = [
76
-        'date' => 'date',
77
-        'due_date' => 'date',
78
-        'approved_at' => 'datetime',
79
-        'paid_at' => 'datetime',
80
-        'last_sent' => 'datetime',
81
-        'status' => InvoiceStatus::class,
82
-        'discount_method' => DocumentDiscountMethod::class,
83
-        'discount_computation' => AdjustmentComputation::class,
84
-        'discount_rate' => RateCast::class,
85
-        'subtotal' => MoneyCast::class,
86
-        'tax_total' => MoneyCast::class,
87
-        'discount_total' => MoneyCast::class,
88
-        'total' => MoneyCast::class,
89
-        'amount_paid' => MoneyCast::class,
90
-        'amount_due' => MoneyCast::class,
91
-    ];
92
-
93
-    public function client(): BelongsTo
94
-    {
95
-        return $this->belongsTo(Client::class);
96
-    }
97
-
98
-    public function currency(): BelongsTo
99
-    {
100
-        return $this->belongsTo(Currency::class, 'currency_code', 'code');
101
-    }
102
-
103
-    public function lineItems(): MorphMany
46
+    protected function casts(): array
104 47
     {
105
-        return $this->morphMany(DocumentLineItem::class, 'documentable');
48
+        return [
49
+            ...parent::casts(),
50
+            'approved_at' => 'datetime',
51
+            'last_sent' => 'datetime',
52
+            'status' => InvoiceStatus::class,
53
+        ];
106 54
     }
107 55
 
108
-    public function transactions(): MorphMany
56
+    public static function documentNumberColumn(): string
109 57
     {
110
-        return $this->morphMany(Transaction::class, 'transactionable');
58
+        return 'invoice_number';
111 59
     }
112 60
 
113
-    public function payments(): MorphMany
61
+    public static function documentType(): DocumentType
114 62
     {
115
-        return $this->transactions()->where('is_payment', true);
63
+        return DocumentType::Invoice;
116 64
     }
117 65
 
118
-    public function deposits(): MorphMany
66
+    public static function getDocumentSettings(): DocumentDefault
119 67
     {
120
-        return $this->transactions()->where('type', TransactionType::Deposit)->where('is_payment', true);
68
+        return auth()->user()->currentCompany->defaultInvoice;
121 69
     }
122 70
 
123
-    public function withdrawals(): MorphMany
71
+    public function client(): BelongsTo
124 72
     {
125
-        return $this->transactions()->where('type', TransactionType::Withdrawal)->where('is_payment', true);
73
+        return $this->belongsTo(Client::class);
126 74
     }
127 75
 
128 76
     public function approvalTransaction(): MorphOne
@@ -141,13 +89,6 @@ class Invoice extends Model
141 89
         ]);
142 90
     }
143 91
 
144
-    protected function isCurrentlyOverdue(): Attribute
145
-    {
146
-        return Attribute::get(function () {
147
-            return $this->due_date->isBefore(today()) && $this->canBeOverdue();
148
-        });
149
-    }
150
-
151 92
     public function isDraft(): bool
152 93
     {
153 94
         return $this->status === InvoiceStatus::Draft;
@@ -177,90 +118,24 @@ class Invoice extends Model
177 118
         return in_array($this->status, InvoiceStatus::canBeOverdue());
178 119
     }
179 120
 
180
-    public function hasPayments(): bool
181
-    {
182
-        return $this->payments->isNotEmpty();
183
-    }
184
-
185
-    public static function getNextDocumentNumber(): string
186
-    {
187
-        $company = auth()->user()->currentCompany;
188
-
189
-        if (! $company) {
190
-            throw new \RuntimeException('No current company is set for the user.');
191
-        }
192
-
193
-        $defaultInvoiceSettings = $company->defaultInvoice;
194
-
195
-        $numberPrefix = $defaultInvoiceSettings->number_prefix;
196
-        $numberDigits = $defaultInvoiceSettings->number_digits;
197
-
198
-        $latestDocument = static::query()
199
-            ->whereNotNull('invoice_number')
200
-            ->latest('invoice_number')
201
-            ->first();
202
-
203
-        $lastNumberNumericPart = $latestDocument
204
-            ? (int) substr($latestDocument->invoice_number, strlen($numberPrefix))
205
-            : 0;
206
-
207
-        $numberNext = $lastNumberNumericPart + 1;
208
-
209
-        return $defaultInvoiceSettings->getNumberNext(
210
-            padded: true,
211
-            format: true,
212
-            prefix: $numberPrefix,
213
-            digits: $numberDigits,
214
-            next: $numberNext
215
-        );
216
-    }
217
-
218 121
     public function recordPayment(array $data): void
219 122
     {
220 123
         $isRefund = $this->status === InvoiceStatus::Overpaid;
221 124
 
222
-        if ($isRefund) {
223
-            $transactionType = TransactionType::Withdrawal;
224
-            $transactionDescription = "Invoice #{$this->invoice_number}: Refund to {$this->client->name}";
225
-        } else {
226
-            $transactionType = TransactionType::Deposit;
227
-            $transactionDescription = "Invoice #{$this->invoice_number}: Payment from {$this->client->name}";
228
-        }
125
+        $transactionType = $isRefund
126
+            ? TransactionType::Withdrawal // Refunds are withdrawals
127
+            : TransactionType::Deposit;  // Payments are deposits
229 128
 
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
-        }
129
+        $transactionDescription = $isRefund
130
+            ? "Invoice #{$this->invoice_number}: Refund to {$this->client->name}"
131
+            : "Invoice #{$this->invoice_number}: Payment from {$this->client->name}";
250 132
 
251
-        // Create transaction
252
-        $this->transactions()->create([
253
-            'company_id' => $this->company_id,
254
-            'type' => $transactionType,
255
-            'is_payment' => true,
256
-            'posted_at' => $data['posted_at'],
257
-            'amount' => $formattedAmountForBankCurrency,
258
-            'payment_method' => $data['payment_method'],
259
-            'bank_account_id' => $data['bank_account_id'],
260
-            'account_id' => Account::getAccountsReceivableAccount()->id,
261
-            'description' => $transactionDescription,
262
-            'notes' => $data['notes'] ?? null,
263
-        ]);
133
+        $this->recordTransaction(
134
+            $data,
135
+            $transactionType,
136
+            $transactionDescription,
137
+            Account::getAccountsReceivableAccount()->id // Account ID specific to invoices
138
+        );
264 139
     }
265 140
 
266 141
     public function approveDraft(?Carbon $approvedAt = null): void
@@ -366,25 +241,6 @@ class Invoice extends Model
366 241
         $this->createApprovalTransaction();
367 242
     }
368 243
 
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
-
388 244
     public static function getApproveDraftAction(string $action = Action::class): MountableAction
389 245
     {
390 246
         return $action::make('approveDraft')

+ 10
- 10
composer.lock 查看文件

@@ -497,16 +497,16 @@
497 497
         },
498 498
         {
499 499
             "name": "aws/aws-sdk-php",
500
-            "version": "3.336.0",
500
+            "version": "3.336.2",
501 501
             "source": {
502 502
                 "type": "git",
503 503
                 "url": "https://github.com/aws/aws-sdk-php.git",
504
-                "reference": "5a62003bf183e32da038056a4b9077c27224e034"
504
+                "reference": "954bfdfc048840ca34afe2a2e1cbcff6681989c4"
505 505
             },
506 506
             "dist": {
507 507
                 "type": "zip",
508
-                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/5a62003bf183e32da038056a4b9077c27224e034",
509
-                "reference": "5a62003bf183e32da038056a4b9077c27224e034",
508
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/954bfdfc048840ca34afe2a2e1cbcff6681989c4",
509
+                "reference": "954bfdfc048840ca34afe2a2e1cbcff6681989c4",
510 510
                 "shasum": ""
511 511
             },
512 512
             "require": {
@@ -589,9 +589,9 @@
589 589
             "support": {
590 590
                 "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
591 591
                 "issues": "https://github.com/aws/aws-sdk-php/issues",
592
-                "source": "https://github.com/aws/aws-sdk-php/tree/3.336.0"
592
+                "source": "https://github.com/aws/aws-sdk-php/tree/3.336.2"
593 593
             },
594
-            "time": "2024-12-18T19:04:32+00:00"
594
+            "time": "2024-12-20T19:05:10+00:00"
595 595
         },
596 596
         {
597 597
             "name": "aws/aws-sdk-php-laravel",
@@ -4707,10 +4707,6 @@
4707 4707
             ],
4708 4708
             "type": "library",
4709 4709
             "extra": {
4710
-                "branch-alias": {
4711
-                    "dev-master": "3.x-dev",
4712
-                    "dev-2.x": "2.x-dev"
4713
-                },
4714 4710
                 "laravel": {
4715 4711
                     "providers": [
4716 4712
                         "Carbon\\Laravel\\ServiceProvider"
@@ -4720,6 +4716,10 @@
4720 4716
                     "includes": [
4721 4717
                         "extension.neon"
4722 4718
                     ]
4719
+                },
4720
+                "branch-alias": {
4721
+                    "dev-2.x": "2.x-dev",
4722
+                    "dev-master": "3.x-dev"
4723 4723
                 }
4724 4724
             },
4725 4725
             "autoload": {

+ 23
- 0
database/factories/Accounting/EstimateFactory.php 查看文件

@@ -0,0 +1,23 @@
1
+<?php
2
+
3
+namespace Database\Factories\Accounting;
4
+
5
+use Illuminate\Database\Eloquent\Factories\Factory;
6
+
7
+/**
8
+ * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Accounting\Estimate>
9
+ */
10
+class EstimateFactory extends Factory
11
+{
12
+    /**
13
+     * Define the model's default state.
14
+     *
15
+     * @return array<string, mixed>
16
+     */
17
+    public function definition(): array
18
+    {
19
+        return [
20
+            //
21
+        ];
22
+    }
23
+}

+ 1
- 1
database/migrations/2024_11_27_221657_create_bills_table.php 查看文件

@@ -15,7 +15,7 @@ return new class extends Migration
15 15
             $table->id();
16 16
             $table->foreignId('company_id')->constrained()->cascadeOnDelete();
17 17
             $table->foreignId('vendor_id')->nullable()->constrained('vendors')->nullOnDelete();
18
-            $table->string('bill_number')->nullable();
18
+            $table->string('number')->nullable();
19 19
             $table->string('order_number')->nullable(); // PO, SO, etc.
20 20
             $table->date('date')->nullable();
21 21
             $table->date('due_date')->nullable();

+ 1
- 1
database/migrations/2024_11_27_223015_create_invoices_table.php 查看文件

@@ -18,7 +18,7 @@ return new class extends Migration
18 18
             $table->string('logo')->nullable();
19 19
             $table->string('header')->nullable();
20 20
             $table->string('subheader')->nullable();
21
-            $table->string('invoice_number')->nullable();
21
+            $table->string('number')->nullable();
22 22
             $table->string('order_number')->nullable(); // PO, SO, etc.
23 23
             $table->date('date')->nullable();
24 24
             $table->date('due_date')->nullable();

+ 52
- 0
database/migrations/2024_12_21_001553_create_estimates_table.php 查看文件

@@ -0,0 +1,52 @@
1
+<?php
2
+
3
+use Illuminate\Database\Migrations\Migration;
4
+use Illuminate\Database\Schema\Blueprint;
5
+use Illuminate\Support\Facades\Schema;
6
+
7
+return new class extends Migration
8
+{
9
+    /**
10
+     * Run the migrations.
11
+     */
12
+    public function up(): void
13
+    {
14
+        Schema::create('estimates', function (Blueprint $table) {
15
+            $table->id();
16
+            $table->foreignId('company_id')->constrained()->cascadeOnDelete();
17
+            $table->foreignId('client_id')->nullable()->constrained('clients')->nullOnDelete();
18
+            $table->string('logo')->nullable();
19
+            $table->string('header')->nullable();
20
+            $table->string('subheader')->nullable();
21
+            $table->string('number')->nullable();
22
+            $table->string('reference_number')->nullable();
23
+            $table->date('date')->nullable();
24
+            $table->date('expiry_date')->nullable();
25
+            $table->timestamp('approved_at')->nullable();
26
+            $table->timestamp('accepted_at')->nullable();
27
+            $table->timestamp('last_sent')->nullable();
28
+            $table->string('status')->default('draft');
29
+            $table->string('currency_code')->nullable();
30
+            $table->string('discount_method')->default('per_line_item');
31
+            $table->string('discount_computation')->default('percentage');
32
+            $table->integer('discount_rate')->default(0);
33
+            $table->integer('subtotal')->default(0);
34
+            $table->integer('tax_total')->default(0);
35
+            $table->integer('discount_total')->default(0);
36
+            $table->integer('total')->default(0);
37
+            $table->text('terms')->nullable(); // terms, notes
38
+            $table->text('footer')->nullable();
39
+            $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
40
+            $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
41
+            $table->timestamps();
42
+        });
43
+    }
44
+
45
+    /**
46
+     * Reverse the migrations.
47
+     */
48
+    public function down(): void
49
+    {
50
+        Schema::dropIfExists('estimates');
51
+    }
52
+};

+ 95
- 92
package-lock.json 查看文件

@@ -558,9 +558,9 @@
558 558
             }
559 559
         },
560 560
         "node_modules/@rollup/rollup-android-arm-eabi": {
561
-            "version": "4.28.1",
562
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz",
563
-            "integrity": "sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==",
561
+            "version": "4.29.0",
562
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.29.0.tgz",
563
+            "integrity": "sha512-TnF0md3qWSRDlU96y9+0dd5RNrlXiQUp1K2pK1UpNmjeND+o9ts9Jxv3G6ntagkt8jVh0KAT1VYgU0nCz5gt2w==",
564 564
             "cpu": [
565 565
                 "arm"
566 566
             ],
@@ -572,9 +572,9 @@
572 572
             ]
573 573
         },
574 574
         "node_modules/@rollup/rollup-android-arm64": {
575
-            "version": "4.28.1",
576
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz",
577
-            "integrity": "sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==",
575
+            "version": "4.29.0",
576
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.29.0.tgz",
577
+            "integrity": "sha512-L/7oX07eY6ACt2NXDrku1JIPdf9VGV/DI92EjAd8FRDzMMub5hXFpT1OegBqimJh9xy9Vv+nToaVtZp4Ku9SEA==",
578 578
             "cpu": [
579 579
                 "arm64"
580 580
             ],
@@ -586,9 +586,9 @@
586 586
             ]
587 587
         },
588 588
         "node_modules/@rollup/rollup-darwin-arm64": {
589
-            "version": "4.28.1",
590
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz",
591
-            "integrity": "sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==",
589
+            "version": "4.29.0",
590
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.29.0.tgz",
591
+            "integrity": "sha512-I1ZucWPVS96hjAsMSJiGosHTqMulMynrmTN+Xde5OsLcU5SjE0xylBmQ/DbB2psJ+HasINrJYz8HQpojtAw2eA==",
592 592
             "cpu": [
593 593
                 "arm64"
594 594
             ],
@@ -600,9 +600,9 @@
600 600
             ]
601 601
         },
602 602
         "node_modules/@rollup/rollup-darwin-x64": {
603
-            "version": "4.28.1",
604
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz",
605
-            "integrity": "sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==",
603
+            "version": "4.29.0",
604
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.29.0.tgz",
605
+            "integrity": "sha512-CTZ+lHMsTbH1q/XLKzmnJWxl2r/1xdv7cnjwbi5v+95nVA1syikxWLvqur4nDoGDHjC8oNMBGurnQptpuFJHXA==",
606 606
             "cpu": [
607 607
                 "x64"
608 608
             ],
@@ -614,9 +614,9 @@
614 614
             ]
615 615
         },
616 616
         "node_modules/@rollup/rollup-freebsd-arm64": {
617
-            "version": "4.28.1",
618
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz",
619
-            "integrity": "sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==",
617
+            "version": "4.29.0",
618
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.29.0.tgz",
619
+            "integrity": "sha512-BB8+4OMzk2JiKL5+aK8A0pi9DPB5pkIBZWXr19+grdez9b0VKihvV432uSwuZLO0sI6zCyxak8NO3mZ1yjM1jA==",
620 620
             "cpu": [
621 621
                 "arm64"
622 622
             ],
@@ -628,9 +628,9 @@
628 628
             ]
629 629
         },
630 630
         "node_modules/@rollup/rollup-freebsd-x64": {
631
-            "version": "4.28.1",
632
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz",
633
-            "integrity": "sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==",
631
+            "version": "4.29.0",
632
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.29.0.tgz",
633
+            "integrity": "sha512-Udz9Uh26uEE6phGMG2++TfpsLK/z4cYJqrIOyVhig/PMoWiZLghpjZUQvsAylsoztbpg0/QmplkDAyyVq0x6Jg==",
634 634
             "cpu": [
635 635
                 "x64"
636 636
             ],
@@ -642,9 +642,9 @@
642 642
             ]
643 643
         },
644 644
         "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
645
-            "version": "4.28.1",
646
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz",
647
-            "integrity": "sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==",
645
+            "version": "4.29.0",
646
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.29.0.tgz",
647
+            "integrity": "sha512-IPSCTzP8GRYzY+siSnggIKrckC2U+kVXoen6eSHRDgU9a4EZCHHWWOiKio1EkieOOk2j6EvZaaHfQUCmt8UJBg==",
648 648
             "cpu": [
649 649
                 "arm"
650 650
             ],
@@ -656,9 +656,9 @@
656 656
             ]
657 657
         },
658 658
         "node_modules/@rollup/rollup-linux-arm-musleabihf": {
659
-            "version": "4.28.1",
660
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz",
661
-            "integrity": "sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==",
659
+            "version": "4.29.0",
660
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.29.0.tgz",
661
+            "integrity": "sha512-GvHPu0UIDx+ohyS8vTYnwoSVMM5BH3NO+JwQs6GWNCuQVlC5rKxnH2WClTGu3NxiIfhKLai08IKUwn3QbzX1UQ==",
662 662
             "cpu": [
663 663
                 "arm"
664 664
             ],
@@ -670,9 +670,9 @@
670 670
             ]
671 671
         },
672 672
         "node_modules/@rollup/rollup-linux-arm64-gnu": {
673
-            "version": "4.28.1",
674
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz",
675
-            "integrity": "sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==",
673
+            "version": "4.29.0",
674
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.29.0.tgz",
675
+            "integrity": "sha512-Pnnn/2CAZWcH9GQoj1nnr85Ejh7aNDe5MsEV0xhuFNUPF0SdnutJ7b2muOI5Kx12T0/i2ol5B/tlhMviZQDL3g==",
676 676
             "cpu": [
677 677
                 "arm64"
678 678
             ],
@@ -684,9 +684,9 @@
684 684
             ]
685 685
         },
686 686
         "node_modules/@rollup/rollup-linux-arm64-musl": {
687
-            "version": "4.28.1",
688
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz",
689
-            "integrity": "sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==",
687
+            "version": "4.29.0",
688
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.29.0.tgz",
689
+            "integrity": "sha512-AP+DLj4q9FT22ZL43ssA3gizEn7/MfJcZ1BOuyEPqoriuH3a8VRuDddN0MtpUwEtiZL6jc1GY5/eL99hkloQ1Q==",
690 690
             "cpu": [
691 691
                 "arm64"
692 692
             ],
@@ -698,9 +698,9 @@
698 698
             ]
699 699
         },
700 700
         "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
701
-            "version": "4.28.1",
702
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.28.1.tgz",
703
-            "integrity": "sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==",
701
+            "version": "4.29.0",
702
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.29.0.tgz",
703
+            "integrity": "sha512-1+jPFClHmDATqbk0Cwi74KEOymVcs09Vbqe/CTKqLwCP0TeP2CACfnMnjYBs5CJgO20e/4bxFtmbR/9fKE1gug==",
704 704
             "cpu": [
705 705
                 "loong64"
706 706
             ],
@@ -712,9 +712,9 @@
712 712
             ]
713 713
         },
714 714
         "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
715
-            "version": "4.28.1",
716
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz",
717
-            "integrity": "sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==",
715
+            "version": "4.29.0",
716
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.29.0.tgz",
717
+            "integrity": "sha512-Nmt5Us5w2dL8eh7QVyAIDVVwBv4wk8ljrBQe7lWkLaOcwABDaFQ3K4sAAC6IsOdJwaXXW+d85zVaMN+Xl8Co2w==",
718 718
             "cpu": [
719 719
                 "ppc64"
720 720
             ],
@@ -726,9 +726,9 @@
726 726
             ]
727 727
         },
728 728
         "node_modules/@rollup/rollup-linux-riscv64-gnu": {
729
-            "version": "4.28.1",
730
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz",
731
-            "integrity": "sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==",
729
+            "version": "4.29.0",
730
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.29.0.tgz",
731
+            "integrity": "sha512-KGuQ8WGhnq09LR7eOru7P9jfBSYXTMhq6TyavWfmEo+TxvkvuRwOCee5lPIa6HYjblOuFr4GeOxSE0c8iyw2Fg==",
732 732
             "cpu": [
733 733
                 "riscv64"
734 734
             ],
@@ -740,9 +740,9 @@
740 740
             ]
741 741
         },
742 742
         "node_modules/@rollup/rollup-linux-s390x-gnu": {
743
-            "version": "4.28.1",
744
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz",
745
-            "integrity": "sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==",
743
+            "version": "4.29.0",
744
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.29.0.tgz",
745
+            "integrity": "sha512-lSQtvrYIONme7a4gbf4O9d3zbZat3/5covIeoqk27ZIkTgBeL/67x+wq2bZfpLjkqQQp5SjBPQ/n0sg8iArzTg==",
746 746
             "cpu": [
747 747
                 "s390x"
748 748
             ],
@@ -754,9 +754,9 @@
754 754
             ]
755 755
         },
756 756
         "node_modules/@rollup/rollup-linux-x64-gnu": {
757
-            "version": "4.28.1",
758
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz",
759
-            "integrity": "sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==",
757
+            "version": "4.29.0",
758
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.29.0.tgz",
759
+            "integrity": "sha512-qh0ussrXBwnF4L07M9t1+jpHRhiGSae+wpNQDbmlXHXciT7pqpZ5zpk4dyGZPtDGB2l2clDiufE16BufXPGRWQ==",
760 760
             "cpu": [
761 761
                 "x64"
762 762
             ],
@@ -768,9 +768,9 @@
768 768
             ]
769 769
         },
770 770
         "node_modules/@rollup/rollup-linux-x64-musl": {
771
-            "version": "4.28.1",
772
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz",
773
-            "integrity": "sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==",
771
+            "version": "4.29.0",
772
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.29.0.tgz",
773
+            "integrity": "sha512-YEABzSaRS7+v14yw6MVBZoMqLoUyTX1/sJoGeC0euvgMrzvw0i+jHo4keDZgYeOblfwdseVAf6ylxWSvcBAKTA==",
774 774
             "cpu": [
775 775
                 "x64"
776 776
             ],
@@ -782,9 +782,9 @@
782 782
             ]
783 783
         },
784 784
         "node_modules/@rollup/rollup-win32-arm64-msvc": {
785
-            "version": "4.28.1",
786
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz",
787
-            "integrity": "sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==",
785
+            "version": "4.29.0",
786
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.29.0.tgz",
787
+            "integrity": "sha512-jA4+oxG7QTTtSQxwSHzFVwShcppHO2DpkbAM59pfD5WMG/da79yQaeBtXAfGTI+ciUx8hqK3RF3H2KWByITXtQ==",
788 788
             "cpu": [
789 789
                 "arm64"
790 790
             ],
@@ -796,9 +796,9 @@
796 796
             ]
797 797
         },
798 798
         "node_modules/@rollup/rollup-win32-ia32-msvc": {
799
-            "version": "4.28.1",
800
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz",
801
-            "integrity": "sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==",
799
+            "version": "4.29.0",
800
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.29.0.tgz",
801
+            "integrity": "sha512-4TQbLoAQVu9uE+cvh47JnjRZylXVdRCoOkRSVF2Rr2T0U1YwphGRjR0sHyRPEt95y3ETT4YFTTzQPq1O4bcjmw==",
802 802
             "cpu": [
803 803
                 "ia32"
804 804
             ],
@@ -810,9 +810,9 @@
810 810
             ]
811 811
         },
812 812
         "node_modules/@rollup/rollup-win32-x64-msvc": {
813
-            "version": "4.28.1",
814
-            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz",
815
-            "integrity": "sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==",
813
+            "version": "4.29.0",
814
+            "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.29.0.tgz",
815
+            "integrity": "sha512-GsFvcTZ7Yj9k94Qm0qgav7pxmQ7lQDR9NjoelRaxeV1UF6JSDfanR/2tHZ8hS7Ps4KPIVf5AElYPRPmN/Q0ZkQ==",
816 816
             "cpu": [
817 817
                 "x64"
818 818
             ],
@@ -1057,9 +1057,9 @@
1057 1057
             }
1058 1058
         },
1059 1059
         "node_modules/caniuse-lite": {
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==",
1060
+            "version": "1.0.30001690",
1061
+            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz",
1062
+            "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==",
1063 1063
             "dev": true,
1064 1064
             "funding": [
1065 1065
                 {
@@ -1218,9 +1218,9 @@
1218 1218
             "license": "MIT"
1219 1219
         },
1220 1220
         "node_modules/electron-to-chromium": {
1221
-            "version": "1.5.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==",
1221
+            "version": "1.5.75",
1222
+            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.75.tgz",
1223
+            "integrity": "sha512-Lf3++DumRE/QmweGjU+ZcKqQ+3bKkU/qjaKYhIJKEOhgIO9Xs6IiAQFkfFoj+RhgDk4LUeNsLo6plExHqSyu6Q==",
1224 1224
             "dev": true,
1225 1225
             "license": "ISC"
1226 1226
         },
@@ -2192,9 +2192,9 @@
2192 2192
             }
2193 2193
         },
2194 2194
         "node_modules/resolve": {
2195
-            "version": "1.22.9",
2196
-            "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.9.tgz",
2197
-            "integrity": "sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A==",
2195
+            "version": "1.22.10",
2196
+            "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
2197
+            "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
2198 2198
             "dev": true,
2199 2199
             "license": "MIT",
2200 2200
             "dependencies": {
@@ -2205,6 +2205,9 @@
2205 2205
             "bin": {
2206 2206
                 "resolve": "bin/resolve"
2207 2207
             },
2208
+            "engines": {
2209
+                "node": ">= 0.4"
2210
+            },
2208 2211
             "funding": {
2209 2212
                 "url": "https://github.com/sponsors/ljharb"
2210 2213
             }
@@ -2221,9 +2224,9 @@
2221 2224
             }
2222 2225
         },
2223 2226
         "node_modules/rollup": {
2224
-            "version": "4.28.1",
2225
-            "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz",
2226
-            "integrity": "sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==",
2227
+            "version": "4.29.0",
2228
+            "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.29.0.tgz",
2229
+            "integrity": "sha512-pdftUn12oB9Qlka+Vpyc39R28D4NsP9Sz6neepSrekofJmWzPD1sxcSO9hEOxFF8+7Kz3sHvwSkkRREI28M1/w==",
2227 2230
             "dev": true,
2228 2231
             "license": "MIT",
2229 2232
             "dependencies": {
@@ -2237,25 +2240,25 @@
2237 2240
                 "npm": ">=8.0.0"
2238 2241
             },
2239 2242
             "optionalDependencies": {
2240
-                "@rollup/rollup-android-arm-eabi": "4.28.1",
2241
-                "@rollup/rollup-android-arm64": "4.28.1",
2242
-                "@rollup/rollup-darwin-arm64": "4.28.1",
2243
-                "@rollup/rollup-darwin-x64": "4.28.1",
2244
-                "@rollup/rollup-freebsd-arm64": "4.28.1",
2245
-                "@rollup/rollup-freebsd-x64": "4.28.1",
2246
-                "@rollup/rollup-linux-arm-gnueabihf": "4.28.1",
2247
-                "@rollup/rollup-linux-arm-musleabihf": "4.28.1",
2248
-                "@rollup/rollup-linux-arm64-gnu": "4.28.1",
2249
-                "@rollup/rollup-linux-arm64-musl": "4.28.1",
2250
-                "@rollup/rollup-linux-loongarch64-gnu": "4.28.1",
2251
-                "@rollup/rollup-linux-powerpc64le-gnu": "4.28.1",
2252
-                "@rollup/rollup-linux-riscv64-gnu": "4.28.1",
2253
-                "@rollup/rollup-linux-s390x-gnu": "4.28.1",
2254
-                "@rollup/rollup-linux-x64-gnu": "4.28.1",
2255
-                "@rollup/rollup-linux-x64-musl": "4.28.1",
2256
-                "@rollup/rollup-win32-arm64-msvc": "4.28.1",
2257
-                "@rollup/rollup-win32-ia32-msvc": "4.28.1",
2258
-                "@rollup/rollup-win32-x64-msvc": "4.28.1",
2243
+                "@rollup/rollup-android-arm-eabi": "4.29.0",
2244
+                "@rollup/rollup-android-arm64": "4.29.0",
2245
+                "@rollup/rollup-darwin-arm64": "4.29.0",
2246
+                "@rollup/rollup-darwin-x64": "4.29.0",
2247
+                "@rollup/rollup-freebsd-arm64": "4.29.0",
2248
+                "@rollup/rollup-freebsd-x64": "4.29.0",
2249
+                "@rollup/rollup-linux-arm-gnueabihf": "4.29.0",
2250
+                "@rollup/rollup-linux-arm-musleabihf": "4.29.0",
2251
+                "@rollup/rollup-linux-arm64-gnu": "4.29.0",
2252
+                "@rollup/rollup-linux-arm64-musl": "4.29.0",
2253
+                "@rollup/rollup-linux-loongarch64-gnu": "4.29.0",
2254
+                "@rollup/rollup-linux-powerpc64le-gnu": "4.29.0",
2255
+                "@rollup/rollup-linux-riscv64-gnu": "4.29.0",
2256
+                "@rollup/rollup-linux-s390x-gnu": "4.29.0",
2257
+                "@rollup/rollup-linux-x64-gnu": "4.29.0",
2258
+                "@rollup/rollup-linux-x64-musl": "4.29.0",
2259
+                "@rollup/rollup-win32-arm64-msvc": "4.29.0",
2260
+                "@rollup/rollup-win32-ia32-msvc": "4.29.0",
2261
+                "@rollup/rollup-win32-x64-msvc": "4.29.0",
2259 2262
                 "fsevents": "~2.3.2"
2260 2263
             }
2261 2264
         },
@@ -2603,13 +2606,13 @@
2603 2606
             "license": "MIT"
2604 2607
         },
2605 2608
         "node_modules/vite": {
2606
-            "version": "6.0.3",
2607
-            "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.3.tgz",
2608
-            "integrity": "sha512-Cmuo5P0ENTN6HxLSo6IHsjCLn/81Vgrp81oaiFFMRa8gGDj5xEjIcEpf2ZymZtZR8oU0P2JX5WuUp/rlXcHkAw==",
2609
+            "version": "6.0.5",
2610
+            "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.5.tgz",
2611
+            "integrity": "sha512-akD5IAH/ID5imgue2DYhzsEwCi0/4VKY31uhMLEYJwPP4TiUp8pL5PIK+Wo7H8qT8JY9i+pVfPydcFPYD1EL7g==",
2609 2612
             "dev": true,
2610 2613
             "license": "MIT",
2611 2614
             "dependencies": {
2612
-                "esbuild": "^0.24.0",
2615
+                "esbuild": "0.24.0",
2613 2616
                 "postcss": "^8.4.49",
2614 2617
                 "rollup": "^4.23.0"
2615 2618
             },

正在加载...
取消
保存