Andrew Wallo 10 月之前
父節點
當前提交
dc0fc6f1d1

+ 46
- 0
app/Casts/DocumentMoneyCast.php 查看文件

1
+<?php
2
+
3
+namespace App\Casts;
4
+
5
+use App\Utilities\Currency\CurrencyAccessor;
6
+use App\Utilities\Currency\CurrencyConverter;
7
+use Illuminate\Contracts\Database\Eloquent\CastsAttributes;
8
+use Illuminate\Database\Eloquent\Model;
9
+use UnexpectedValueException;
10
+
11
+class DocumentMoneyCast implements CastsAttributes
12
+{
13
+    /**
14
+     * Cast the given value.
15
+     *
16
+     * @param  array<string, mixed>  $attributes
17
+     */
18
+    public function get(Model $model, string $key, mixed $value, array $attributes): mixed
19
+    {
20
+        $currency_code = $attributes['currency_code'] ?? CurrencyAccessor::getDefaultCurrency();
21
+
22
+        if ($value !== null) {
23
+            return CurrencyConverter::convertCentsToFloat($value, $currency_code);
24
+        }
25
+
26
+        return 0.0;
27
+    }
28
+
29
+    /**
30
+     * Prepare the given value for storage.
31
+     *
32
+     * @param  array<string, mixed>  $attributes
33
+     */
34
+    public function set(Model $model, string $key, mixed $value, array $attributes): mixed
35
+    {
36
+        $currency_code = $attributes['currency_code'] ?? CurrencyAccessor::getDefaultCurrency();
37
+
38
+        if (is_numeric($value)) {
39
+            $value = (string) $value;
40
+        } elseif (! is_string($value)) {
41
+            throw new UnexpectedValueException('Expected string or numeric value for money cast');
42
+        }
43
+
44
+        return CurrencyConverter::prepareForAccessor($value, $currency_code);
45
+    }
46
+}

+ 60
- 0
app/Enums/Accounting/DocumentStatus.php 查看文件

1
+<?php
2
+
3
+namespace App\Enums\Accounting;
4
+
5
+use Filament\Support\Contracts\HasColor;
6
+use Filament\Support\Contracts\HasLabel;
7
+
8
+enum DocumentStatus: string implements HasColor, HasLabel
9
+{
10
+    case Draft = 'draft';
11
+    case Sent = 'sent';
12
+
13
+    case Partial = 'partial';
14
+
15
+    case Paid = 'paid';
16
+
17
+    case Overdue = 'overdue';
18
+
19
+    case Void = 'void';
20
+
21
+    case Unpaid = 'unpaid';
22
+
23
+    public function getInvoiceStatuses(): array
24
+    {
25
+        return [
26
+            self::Draft,
27
+            self::Sent,
28
+            self::Partial,
29
+            self::Paid,
30
+            self::Overdue,
31
+            self::Void,
32
+        ];
33
+    }
34
+
35
+    public function getBillStatuses(): array
36
+    {
37
+        return [
38
+            self::Partial,
39
+            self::Paid,
40
+            self::Unpaid,
41
+            self::Void,
42
+        ];
43
+    }
44
+
45
+    public function getLabel(): ?string
46
+    {
47
+        return $this->name;
48
+    }
49
+
50
+    public function getColor(): string | array | null
51
+    {
52
+        return match ($this) {
53
+            self::Draft, self::Void => 'gray',
54
+            self::Sent => 'primary',
55
+            self::Partial, self::Unpaid => 'warning',
56
+            self::Paid => 'success',
57
+            self::Overdue => 'danger',
58
+        };
59
+    }
60
+}

+ 139
- 41
app/Filament/Company/Resources/Accounting/DocumentResource.php 查看文件

6
 use App\Filament\Company\Resources\Accounting\DocumentResource\Pages;
6
 use App\Filament\Company\Resources\Accounting\DocumentResource\Pages;
7
 use App\Models\Accounting\Adjustment;
7
 use App\Models\Accounting\Adjustment;
8
 use App\Models\Accounting\Document;
8
 use App\Models\Accounting\Document;
9
+use App\Models\Accounting\DocumentLineItem;
9
 use App\Models\Common\Offering;
10
 use App\Models\Common\Offering;
10
 use App\Utilities\Currency\CurrencyAccessor;
11
 use App\Utilities\Currency\CurrencyAccessor;
11
 use Awcodes\TableRepeater\Components\TableRepeater;
12
 use Awcodes\TableRepeater\Components\TableRepeater;
12
 use Awcodes\TableRepeater\Header;
13
 use Awcodes\TableRepeater\Header;
14
+use Carbon\CarbonInterface;
13
 use Filament\Forms;
15
 use Filament\Forms;
14
 use Filament\Forms\Components\FileUpload;
16
 use Filament\Forms\Components\FileUpload;
15
 use Filament\Forms\Form;
17
 use Filament\Forms\Form;
17
 use Filament\Support\Enums\MaxWidth;
19
 use Filament\Support\Enums\MaxWidth;
18
 use Filament\Tables;
20
 use Filament\Tables;
19
 use Filament\Tables\Table;
21
 use Filament\Tables\Table;
22
+use Illuminate\Database\Eloquent\Model;
23
+use Illuminate\Support\Carbon;
20
 use Illuminate\Support\Facades\Auth;
24
 use Illuminate\Support\Facades\Auth;
21
 use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
25
 use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
22
 
26
 
88
                             Forms\Components\Group::make([
92
                             Forms\Components\Group::make([
89
                                 Forms\Components\TextInput::make('document_number')
93
                                 Forms\Components\TextInput::make('document_number')
90
                                     ->label('Invoice Number')
94
                                     ->label('Invoice Number')
91
-                                    ->default(fn () => $company->defaultInvoice->getNumberNext()),
95
+                                    ->default(fn () => Document::getNextDocumentNumber()),
92
                                 Forms\Components\TextInput::make('order_number')
96
                                 Forms\Components\TextInput::make('order_number')
93
                                     ->label('P.O/S.O Number'),
97
                                     ->label('P.O/S.O Number'),
94
                                 Forms\Components\DatePicker::make('date')
98
                                 Forms\Components\DatePicker::make('date')
103
                         ])->from('md'),
107
                         ])->from('md'),
104
                         TableRepeater::make('lineItems')
108
                         TableRepeater::make('lineItems')
105
                             ->relationship()
109
                             ->relationship()
106
-                            ->saveRelationshipsUsing(function (Document $document, array $state) {
107
-                                $document->lineItems()->delete();
108
-
109
-                                collect($state)->map(function ($lineItemData) use ($document) {
110
-                                    $lineItem = $document->lineItems()->create([
111
-                                        'offering_id' => $lineItemData['offering_id'],
112
-                                        'description' => $lineItemData['description'],
113
-                                        'quantity' => $lineItemData['quantity'],
114
-                                        'unit_price' => $lineItemData['unit_price'],
115
-                                        'tax_total' => collect($lineItemData['salesTaxes'] ?? [])->sum(function ($taxId) use ($lineItemData) {
116
-                                            $tax = Adjustment::find($taxId);
117
-
118
-                                            return $tax ? ($lineItemData['quantity'] * $lineItemData['unit_price']) * ($tax->rate / 100) : 0;
119
-                                        }),
120
-                                    ]);
110
+                            ->saveRelationshipsUsing(function (TableRepeater $component, Forms\Contracts\HasForms $livewire, ?array $state) {
111
+                                if (! is_array($state)) {
112
+                                    $state = [];
113
+                                }
114
+
115
+                                $relationship = $component->getRelationship();
116
+
117
+                                $existingRecords = $component->getCachedExistingRecords();
118
+
119
+                                $recordsToDelete = [];
120
+
121
+                                foreach ($existingRecords->pluck($relationship->getRelated()->getKeyName()) as $keyToCheckForDeletion) {
122
+                                    if (array_key_exists("record-{$keyToCheckForDeletion}", $state)) {
123
+                                        continue;
124
+                                    }
125
+
126
+                                    $recordsToDelete[] = $keyToCheckForDeletion;
127
+                                    $existingRecords->forget("record-{$keyToCheckForDeletion}");
128
+                                }
129
+
130
+                                $relationship
131
+                                    ->whereKey($recordsToDelete)
132
+                                    ->get()
133
+                                    ->each(static fn (Model $record) => $record->delete());
134
+
135
+                                $childComponentContainers = $component->getChildComponentContainers(
136
+                                    withHidden: $component->shouldSaveRelationshipsWhenHidden(),
137
+                                );
138
+
139
+                                $itemOrder = 1;
140
+                                $orderColumn = $component->getOrderColumn();
141
+
142
+                                $translatableContentDriver = $livewire->makeFilamentTranslatableContentDriver();
143
+
144
+                                foreach ($childComponentContainers as $itemKey => $item) {
145
+                                    $itemData = $item->getState(shouldCallHooksBefore: false);
146
+
147
+                                    if ($orderColumn) {
148
+                                        $itemData[$orderColumn] = $itemOrder;
149
+
150
+                                        $itemOrder++;
151
+                                    }
152
+
153
+                                    if ($record = ($existingRecords[$itemKey] ?? null)) {
154
+                                        $itemData = $component->mutateRelationshipDataBeforeSave($itemData, record: $record);
155
+
156
+                                        if ($itemData === null) {
157
+                                            continue;
158
+                                        }
159
+
160
+                                        $translatableContentDriver ?
161
+                                            $translatableContentDriver->updateRecord($record, $itemData) :
162
+                                            $record->fill($itemData)->save();
163
+
164
+                                        continue;
165
+                                    }
166
+
167
+                                    $relatedModel = $component->getRelatedModel();
121
 
168
 
122
-                                    $lineItem->taxes()->sync($lineItemData['salesTaxes'] ?? []);
169
+                                    $itemData = $component->mutateRelationshipDataBeforeCreate($itemData);
123
 
170
 
124
-                                    return $lineItem;
171
+                                    if ($itemData === null) {
172
+                                        continue;
173
+                                    }
174
+
175
+                                    if ($translatableContentDriver) {
176
+                                        $record = $translatableContentDriver->makeRecord($relatedModel, $itemData);
177
+                                    } else {
178
+                                        $record = new $relatedModel;
179
+                                        $record->fill($itemData);
180
+                                    }
181
+
182
+                                    $record = $relationship->save($record);
183
+                                    $item->model($record)->saveRelationships();
184
+                                    $existingRecords->push($record);
185
+                                }
186
+
187
+                                $component->getRecord()->setRelation($component->getRelationshipName(), $existingRecords);
188
+
189
+                                /** @var Document $document */
190
+                                $document = $component->getRecord();
191
+
192
+                                // Recalculate totals for line items
193
+                                $document->lineItems()->each(function (DocumentLineItem $lineItem) {
194
+                                    $lineItem->updateQuietly([
195
+                                        'tax_total' => $lineItem->calculateTaxTotal()->getAmount(),
196
+                                        'discount_total' => $lineItem->calculateDiscountTotal()->getAmount(),
197
+                                    ]);
125
                                 });
198
                                 });
126
 
199
 
127
-                                $document->refresh();
128
                                 $subtotal = $document->lineItems()->sum('subtotal') / 100;
200
                                 $subtotal = $document->lineItems()->sum('subtotal') / 100;
129
                                 $taxTotal = $document->lineItems()->sum('tax_total') / 100;
201
                                 $taxTotal = $document->lineItems()->sum('tax_total') / 100;
130
                                 $discountTotal = $document->lineItems()->sum('discount_total') / 100;
202
                                 $discountTotal = $document->lineItems()->sum('discount_total') / 100;
138
                                 ]);
210
                                 ]);
139
                             })
211
                             })
140
                             ->headers([
212
                             ->headers([
141
-                                Header::make('Items')->width('20%'),
142
-                                Header::make('Description')->width('30%'),
213
+                                Header::make('Items')->width('15%'),
214
+                                Header::make('Description')->width('25%'),
143
                                 Header::make('Quantity')->width('10%'),
215
                                 Header::make('Quantity')->width('10%'),
144
                                 Header::make('Price')->width('10%'),
216
                                 Header::make('Price')->width('10%'),
145
-                                Header::make('Taxes')->width('20%'),
217
+                                Header::make('Taxes')->width('15%'),
218
+                                Header::make('Discounts')->width('15%'),
146
                                 Header::make('Amount')->width('10%')->align('right'),
219
                                 Header::make('Amount')->width('10%')->align('right'),
147
                             ])
220
                             ])
148
                             ->live()
221
                             ->live()
160
                                             $set('description', $offeringRecord->description);
233
                                             $set('description', $offeringRecord->description);
161
                                             $set('unit_price', $offeringRecord->price);
234
                                             $set('unit_price', $offeringRecord->price);
162
                                             $set('salesTaxes', $offeringRecord->salesTaxes->pluck('id')->toArray());
235
                                             $set('salesTaxes', $offeringRecord->salesTaxes->pluck('id')->toArray());
163
-
164
-                                            $quantity = $get('quantity');
165
-                                            $total = $quantity * $offeringRecord->price;
166
-
167
-                                            // Calculate taxes and update total
168
-                                            $taxAmount = $offeringRecord->salesTaxes->sum(fn ($tax) => $total * ($tax->rate / 100));
169
-                                            $set('total', $total + $taxAmount);
236
+                                            $set('salesDiscounts', $offeringRecord->salesDiscounts->pluck('id')->toArray());
170
                                         }
237
                                         }
171
                                     }),
238
                                     }),
172
                                 Forms\Components\TextInput::make('description'),
239
                                 Forms\Components\TextInput::make('description'),
183
                                     ->preload()
250
                                     ->preload()
184
                                     ->multiple()
251
                                     ->multiple()
185
                                     ->searchable(),
252
                                     ->searchable(),
253
+                                Forms\Components\Select::make('salesDiscounts')
254
+                                    ->relationship('salesDiscounts', 'name')
255
+                                    ->preload()
256
+                                    ->multiple()
257
+                                    ->searchable(),
186
                                 Forms\Components\Placeholder::make('total')
258
                                 Forms\Components\Placeholder::make('total')
187
                                     ->hiddenLabel()
259
                                     ->hiddenLabel()
188
                                     ->content(function (Forms\Get $get) {
260
                                     ->content(function (Forms\Get $get) {
189
                                         $quantity = $get('quantity') ?? 0;
261
                                         $quantity = $get('quantity') ?? 0;
190
                                         $unitPrice = $get('unit_price') ?? 0;
262
                                         $unitPrice = $get('unit_price') ?? 0;
191
                                         $salesTaxes = $get('salesTaxes') ?? [];
263
                                         $salesTaxes = $get('salesTaxes') ?? [];
264
+                                        $salesDiscounts = $get('salesDiscounts') ?? [];
192
 
265
 
193
-                                        $total = $quantity * $unitPrice;
266
+                                        // Base total (subtotal)
267
+                                        $subtotal = $quantity * $unitPrice;
194
 
268
 
269
+                                        // Calculate tax amount based on subtotal
270
+                                        $taxAmount = 0;
195
                                         if (! empty($salesTaxes)) {
271
                                         if (! empty($salesTaxes)) {
196
                                             $taxRates = Adjustment::whereIn('id', $salesTaxes)->pluck('rate');
272
                                             $taxRates = Adjustment::whereIn('id', $salesTaxes)->pluck('rate');
273
+                                            $taxAmount = collect($taxRates)->sum(fn ($rate) => $subtotal * ($rate / 100));
274
+                                        }
197
 
275
 
198
-                                            $taxAmount = $taxRates->sum(function ($rate) use ($total) {
199
-                                                return $total * ($rate / 100);
200
-                                            });
201
-
202
-                                            $total += $taxAmount;
203
-
204
-                                            return money($total, CurrencyAccessor::getDefaultCurrency(), true)->format();
276
+                                        // Calculate discount amount based on subtotal
277
+                                        $discountAmount = 0;
278
+                                        if (! empty($salesDiscounts)) {
279
+                                            $discountRates = Adjustment::whereIn('id', $salesDiscounts)->pluck('rate');
280
+                                            $discountAmount = collect($discountRates)->sum(fn ($rate) => $subtotal * ($rate / 100));
205
                                         }
281
                                         }
206
 
282
 
283
+                                        // Final total
284
+                                        $total = $subtotal + ($taxAmount - $discountAmount);
285
+
207
                                         return money($total, CurrencyAccessor::getDefaultCurrency(), true)->format();
286
                                         return money($total, CurrencyAccessor::getDefaultCurrency(), true)->format();
208
                                     }),
287
                                     }),
209
                             ]),
288
                             ]),
316
         return $table
395
         return $table
317
             ->columns([
396
             ->columns([
318
                 Tables\Columns\TextColumn::make('status')
397
                 Tables\Columns\TextColumn::make('status')
398
+                    ->badge()
319
                     ->searchable(),
399
                     ->searchable(),
320
                 Tables\Columns\TextColumn::make('due_date')
400
                 Tables\Columns\TextColumn::make('due_date')
321
-                    ->date()
401
+                    ->label('Due')
402
+                    ->formatStateUsing(function (Tables\Columns\TextColumn $column, mixed $state) {
403
+                        if (blank($state)) {
404
+                            return null;
405
+                        }
406
+
407
+                        $date = Carbon::parse($state)
408
+                            ->setTimezone($timezone ?? $column->getTimezone());
409
+
410
+                        if ($date->isToday()) {
411
+                            return 'Today';
412
+                        }
413
+
414
+                        return $date->diffForHumans([
415
+                            'options' => CarbonInterface::ONE_DAY_WORDS,
416
+                        ]);
417
+                    })
322
                     ->sortable(),
418
                     ->sortable(),
323
                 Tables\Columns\TextColumn::make('date')
419
                 Tables\Columns\TextColumn::make('date')
324
                     ->date()
420
                     ->date()
330
                     ->numeric()
426
                     ->numeric()
331
                     ->sortable(),
427
                     ->sortable(),
332
                 Tables\Columns\TextColumn::make('total')
428
                 Tables\Columns\TextColumn::make('total')
333
-                    ->money(),
429
+                    ->currency(),
334
                 Tables\Columns\TextColumn::make('amount_paid')
430
                 Tables\Columns\TextColumn::make('amount_paid')
335
-                    ->money(),
431
+                    ->label('Amount Paid')
432
+                    ->currency(),
336
                 Tables\Columns\TextColumn::make('amount_due')
433
                 Tables\Columns\TextColumn::make('amount_due')
337
-                    ->money(),
434
+                    ->label('Amount Due')
435
+                    ->currency(),
338
             ])
436
             ])
339
             ->filters([
437
             ->filters([
340
                 //
438
                 //

+ 3
- 5
app/Filament/Company/Resources/Sales/InvoiceResource/Pages/EditInvoice.php 查看文件

5
 use App\Filament\Company\Resources\Sales\InvoiceResource;
5
 use App\Filament\Company\Resources\Sales\InvoiceResource;
6
 use Filament\Actions;
6
 use Filament\Actions;
7
 use Filament\Resources\Pages\EditRecord;
7
 use Filament\Resources\Pages\EditRecord;
8
-use Illuminate\Database\Eloquent\Model;
8
+use Filament\Support\Enums\MaxWidth;
9
 
9
 
10
 class EditInvoice extends EditRecord
10
 class EditInvoice extends EditRecord
11
 {
11
 {
18
         ];
18
         ];
19
     }
19
     }
20
 
20
 
21
-    protected function handleRecordUpdate(Model $record, array $data): Model
21
+    public function getMaxContentWidth(): MaxWidth | string | null
22
     {
22
     {
23
-        ray($data);
24
-
25
-        return parent::handleRecordUpdate($record, $data); // TODO: Change the autogenerated stub
23
+        return MaxWidth::Full;
26
     }
24
     }
27
 }
25
 }

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

5
 use App\Casts\MoneyCast;
5
 use App\Casts\MoneyCast;
6
 use App\Concerns\Blamable;
6
 use App\Concerns\Blamable;
7
 use App\Concerns\CompanyOwned;
7
 use App\Concerns\CompanyOwned;
8
+use App\Enums\Accounting\DocumentStatus;
8
 use App\Enums\Accounting\DocumentType;
9
 use App\Enums\Accounting\DocumentType;
9
 use App\Models\Banking\Payment;
10
 use App\Models\Banking\Payment;
10
 use App\Models\Common\Client;
11
 use App\Models\Common\Client;
54
         'type' => DocumentType::class,
55
         'type' => DocumentType::class,
55
         'date' => 'date',
56
         'date' => 'date',
56
         'due_date' => 'date',
57
         'due_date' => 'date',
58
+        'status' => DocumentStatus::class,
57
         'subtotal' => MoneyCast::class,
59
         'subtotal' => MoneyCast::class,
58
         'tax_total' => MoneyCast::class,
60
         'tax_total' => MoneyCast::class,
59
         'discount_total' => MoneyCast::class,
61
         'discount_total' => MoneyCast::class,
81
     {
83
     {
82
         return $this->hasMany(Payment::class);
84
         return $this->hasMany(Payment::class);
83
     }
85
     }
86
+
87
+    public static function getNextDocumentNumber(DocumentType $documentType = DocumentType::Invoice): string
88
+    {
89
+        $company = auth()->user()->currentCompany;
90
+
91
+        if (! $company) {
92
+            throw new \RuntimeException('No current company is set for the user.');
93
+        }
94
+
95
+        $defaultInvoiceSettings = $company->defaultInvoice;
96
+
97
+        $numberPrefix = $defaultInvoiceSettings->number_prefix;
98
+        $numberDigits = $defaultInvoiceSettings->number_digits;
99
+
100
+        $latestDocument = static::query()
101
+            ->whereNotNull('document_number')
102
+            ->where('type', $documentType)
103
+            ->latest('document_number')
104
+            ->first();
105
+
106
+        $lastNumberNumericPart = $latestDocument
107
+            ? (int) substr($latestDocument->document_number, strlen($numberPrefix))
108
+            : 0;
109
+
110
+        $numberNext = $lastNumberNumericPart + 1;
111
+
112
+        return $defaultInvoiceSettings->getNumberNext(
113
+            padded: true,
114
+            format: true,
115
+            prefix: $numberPrefix,
116
+            digits: $numberDigits,
117
+            next: $numberNext
118
+        );
119
+    }
84
 }
120
 }

+ 24
- 1
app/Models/Accounting/DocumentLineItem.php 查看文件

2
 
2
 
3
 namespace App\Models\Accounting;
3
 namespace App\Models\Accounting;
4
 
4
 
5
+use Akaunting\Money\Money;
6
+use App\Casts\DocumentMoneyCast;
5
 use App\Casts\MoneyCast;
7
 use App\Casts\MoneyCast;
6
 use App\Concerns\Blamable;
8
 use App\Concerns\Blamable;
7
 use App\Concerns\CompanyOwned;
9
 use App\Concerns\CompanyOwned;
9
 use App\Enums\Accounting\AdjustmentType;
11
 use App\Enums\Accounting\AdjustmentType;
10
 use App\Models\Common\Offering;
12
 use App\Models\Common\Offering;
11
 use App\Observers\DocumentLineItemObserver;
13
 use App\Observers\DocumentLineItemObserver;
14
+use App\Utilities\Currency\CurrencyAccessor;
12
 use Illuminate\Database\Eloquent\Attributes\ObservedBy;
15
 use Illuminate\Database\Eloquent\Attributes\ObservedBy;
13
 use Illuminate\Database\Eloquent\Factories\HasFactory;
16
 use Illuminate\Database\Eloquent\Factories\HasFactory;
14
 use Illuminate\Database\Eloquent\Model;
17
 use Illuminate\Database\Eloquent\Model;
39
 
42
 
40
     protected $casts = [
43
     protected $casts = [
41
         'unit_price' => MoneyCast::class,
44
         'unit_price' => MoneyCast::class,
42
-        'subtotal' => MoneyCast::class,
45
+        'subtotal' => DocumentMoneyCast::class,
43
         'tax_total' => MoneyCast::class,
46
         'tax_total' => MoneyCast::class,
44
         'discount_total' => MoneyCast::class,
47
         'discount_total' => MoneyCast::class,
45
         'total' => MoneyCast::class,
48
         'total' => MoneyCast::class,
79
     {
82
     {
80
         return $this->adjustments()->where('category', AdjustmentCategory::Discount);
83
         return $this->adjustments()->where('category', AdjustmentCategory::Discount);
81
     }
84
     }
85
+
86
+    public function calculateTaxTotal(): Money
87
+    {
88
+        $subtotal = money($this->subtotal, CurrencyAccessor::getDefaultCurrency());
89
+
90
+        return $this->taxes->reduce(
91
+            fn (Money $carry, Adjustment $tax) => $carry->add($subtotal->multiply($tax->rate / 100)),
92
+            money(0, CurrencyAccessor::getDefaultCurrency())
93
+        );
94
+    }
95
+
96
+    public function calculateDiscountTotal(): Money
97
+    {
98
+        $subtotal = money($this->subtotal, CurrencyAccessor::getDefaultCurrency());
99
+
100
+        return $this->discounts->reduce(
101
+            fn (Money $carry, Adjustment $discount) => $carry->add($subtotal->multiply($discount->rate / 100)),
102
+            money(0, CurrencyAccessor::getDefaultCurrency())
103
+        );
104
+    }
82
 }
105
 }

+ 1
- 1
app/Observers/DocumentLineItemObserver.php 查看文件

27
      */
27
      */
28
     public function deleted(DocumentLineItem $documentLineItem): void
28
     public function deleted(DocumentLineItem $documentLineItem): void
29
     {
29
     {
30
-        //
30
+        $documentLineItem->adjustments()->detach();
31
     }
31
     }
32
 
32
 
33
     /**
33
     /**

+ 3
- 0
app/Providers/MacroServiceProvider.php 查看文件

81
         });
81
         });
82
 
82
 
83
         TextColumn::macro('currency', function (string | Closure | null $currency = null, ?bool $convert = null): static {
83
         TextColumn::macro('currency', function (string | Closure | null $currency = null, ?bool $convert = null): static {
84
+            $currency ??= CurrencyAccessor::getDefaultCurrency();
85
+            $convert ??= true;
86
+
84
             $this->formatStateUsing(static function (TextColumn $column, $state) use ($currency, $convert): ?string {
87
             $this->formatStateUsing(static function (TextColumn $column, $state) use ($currency, $convert): ?string {
85
                 if (blank($state)) {
88
                 if (blank($state)) {
86
                     return null;
89
                     return null;

+ 7
- 0
app/Utilities/Currency/CurrencyConverter.php 查看文件

56
         return money($amount, $currency, true)->format();
56
         return money($amount, $currency, true)->format();
57
     }
57
     }
58
 
58
 
59
+    public static function convertCentsToFloat(int $amount, ?string $currency = null): float
60
+    {
61
+        $currency ??= CurrencyAccessor::getDefaultCurrency();
62
+
63
+        return money($amount, $currency)->getValue();
64
+    }
65
+
59
     public static function handleCurrencyChange(Set $set, $state): void
66
     public static function handleCurrencyChange(Set $set, $state): void
60
     {
67
     {
61
         $currency = currency($state);
68
         $currency = currency($state);

+ 16
- 1
app/View/Models/InvoiceTotalViewModel.php 查看文件

32
             return $carry + $taxAmount;
32
             return $carry + $taxAmount;
33
         }, 0);
33
         }, 0);
34
 
34
 
35
-        $grandTotal = $subtotal + $taxTotal;
35
+        $discountTotal = $lineItems->reduce(function ($carry, $item) {
36
+            $quantity = $item['quantity'] ?? 0;
37
+            $unitPrice = $item['unit_price'] ?? 0;
38
+            $salesDiscounts = $item['salesDiscounts'] ?? [];
39
+            $lineTotal = $quantity * $unitPrice;
40
+
41
+            $discountAmount = Adjustment::whereIn('id', $salesDiscounts)
42
+                ->pluck('rate')
43
+                ->sum(fn ($rate) => $lineTotal * ($rate / 100));
44
+
45
+            return $carry + $discountAmount;
46
+        }, 0);
47
+
48
+        $grandTotal = $subtotal + ($taxTotal - $discountTotal);
36
 
49
 
37
         $subTotalFormatted = CurrencyConverter::formatToMoney($subtotal);
50
         $subTotalFormatted = CurrencyConverter::formatToMoney($subtotal);
38
         $taxTotalFormatted = CurrencyConverter::formatToMoney($taxTotal);
51
         $taxTotalFormatted = CurrencyConverter::formatToMoney($taxTotal);
52
+        $discountTotalFormatted = CurrencyConverter::formatToMoney($discountTotal);
39
         $grandTotalFormatted = CurrencyConverter::formatToMoney($grandTotal);
53
         $grandTotalFormatted = CurrencyConverter::formatToMoney($grandTotal);
40
 
54
 
41
         return [
55
         return [
42
             'subtotal' => $subTotalFormatted,
56
             'subtotal' => $subTotalFormatted,
43
             'taxTotal' => $taxTotalFormatted,
57
             'taxTotal' => $taxTotalFormatted,
58
+            'discountTotal' => $discountTotalFormatted,
44
             'grandTotal' => $grandTotalFormatted,
59
             'grandTotal' => $grandTotalFormatted,
45
         ];
60
         ];
46
     }
61
     }

+ 79
- 75
composer.lock 查看文件

497
         },
497
         },
498
         {
498
         {
499
             "name": "aws/aws-sdk-php",
499
             "name": "aws/aws-sdk-php",
500
-            "version": "3.330.1",
500
+            "version": "3.330.2",
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": "136749f15d1dbff07064ef5ba1c2f08b96cf78ff"
504
+                "reference": "4ac43cc8356fb16a494c3631c8f39f6e7555f00a"
505
             },
505
             },
506
             "dist": {
506
             "dist": {
507
                 "type": "zip",
507
                 "type": "zip",
508
-                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/136749f15d1dbff07064ef5ba1c2f08b96cf78ff",
509
-                "reference": "136749f15d1dbff07064ef5ba1c2f08b96cf78ff",
508
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/4ac43cc8356fb16a494c3631c8f39f6e7555f00a",
509
+                "reference": "4ac43cc8356fb16a494c3631c8f39f6e7555f00a",
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.330.1"
592
+                "source": "https://github.com/aws/aws-sdk-php/tree/3.330.2"
593
             },
593
             },
594
-            "time": "2024-11-25T19:20:00+00:00"
594
+            "time": "2024-11-26T19:07:56+00:00"
595
         },
595
         },
596
         {
596
         {
597
             "name": "aws/aws-sdk-php-laravel",
597
             "name": "aws/aws-sdk-php-laravel",
2852
         },
2852
         },
2853
         {
2853
         {
2854
             "name": "kirschbaum-development/eloquent-power-joins",
2854
             "name": "kirschbaum-development/eloquent-power-joins",
2855
-            "version": "4.0.0",
2855
+            "version": "4.0.1",
2856
             "source": {
2856
             "source": {
2857
                 "type": "git",
2857
                 "type": "git",
2858
                 "url": "https://github.com/kirschbaum-development/eloquent-power-joins.git",
2858
                 "url": "https://github.com/kirschbaum-development/eloquent-power-joins.git",
2859
-                "reference": "c6c42a52c5a097cc11761e72782b2d0215692caf"
2859
+                "reference": "3c1af9b86b02f1e39219849c1d2fee7cf77e8638"
2860
             },
2860
             },
2861
             "dist": {
2861
             "dist": {
2862
                 "type": "zip",
2862
                 "type": "zip",
2863
-                "url": "https://api.github.com/repos/kirschbaum-development/eloquent-power-joins/zipball/c6c42a52c5a097cc11761e72782b2d0215692caf",
2864
-                "reference": "c6c42a52c5a097cc11761e72782b2d0215692caf",
2863
+                "url": "https://api.github.com/repos/kirschbaum-development/eloquent-power-joins/zipball/3c1af9b86b02f1e39219849c1d2fee7cf77e8638",
2864
+                "reference": "3c1af9b86b02f1e39219849c1d2fee7cf77e8638",
2865
                 "shasum": ""
2865
                 "shasum": ""
2866
             },
2866
             },
2867
             "require": {
2867
             "require": {
2909
             ],
2909
             ],
2910
             "support": {
2910
             "support": {
2911
                 "issues": "https://github.com/kirschbaum-development/eloquent-power-joins/issues",
2911
                 "issues": "https://github.com/kirschbaum-development/eloquent-power-joins/issues",
2912
-                "source": "https://github.com/kirschbaum-development/eloquent-power-joins/tree/4.0.0"
2912
+                "source": "https://github.com/kirschbaum-development/eloquent-power-joins/tree/4.0.1"
2913
             },
2913
             },
2914
-            "time": "2024-10-06T12:28:14+00:00"
2914
+            "time": "2024-11-26T13:22:08+00:00"
2915
         },
2915
         },
2916
         {
2916
         {
2917
             "name": "knplabs/knp-snappy",
2917
             "name": "knplabs/knp-snappy",
2982
         },
2982
         },
2983
         {
2983
         {
2984
             "name": "laravel/framework",
2984
             "name": "laravel/framework",
2985
-            "version": "v11.33.2",
2985
+            "version": "v11.34.1",
2986
             "source": {
2986
             "source": {
2987
                 "type": "git",
2987
                 "type": "git",
2988
                 "url": "https://github.com/laravel/framework.git",
2988
                 "url": "https://github.com/laravel/framework.git",
2989
-                "reference": "6b9832751cf8eed18b3c73df5071f78f0682aa5d"
2989
+                "reference": "ed07324892c87277b7d37ba76b1a6f93a37401b5"
2990
             },
2990
             },
2991
             "dist": {
2991
             "dist": {
2992
                 "type": "zip",
2992
                 "type": "zip",
2993
-                "url": "https://api.github.com/repos/laravel/framework/zipball/6b9832751cf8eed18b3c73df5071f78f0682aa5d",
2994
-                "reference": "6b9832751cf8eed18b3c73df5071f78f0682aa5d",
2993
+                "url": "https://api.github.com/repos/laravel/framework/zipball/ed07324892c87277b7d37ba76b1a6f93a37401b5",
2994
+                "reference": "ed07324892c87277b7d37ba76b1a6f93a37401b5",
2995
                 "shasum": ""
2995
                 "shasum": ""
2996
             },
2996
             },
2997
             "require": {
2997
             "require": {
2998
                 "brick/math": "^0.9.3|^0.10.2|^0.11|^0.12",
2998
                 "brick/math": "^0.9.3|^0.10.2|^0.11|^0.12",
2999
                 "composer-runtime-api": "^2.2",
2999
                 "composer-runtime-api": "^2.2",
3000
                 "doctrine/inflector": "^2.0.5",
3000
                 "doctrine/inflector": "^2.0.5",
3001
-                "dragonmantank/cron-expression": "^3.3.2",
3001
+                "dragonmantank/cron-expression": "^3.4",
3002
                 "egulias/email-validator": "^3.2.1|^4.0",
3002
                 "egulias/email-validator": "^3.2.1|^4.0",
3003
                 "ext-ctype": "*",
3003
                 "ext-ctype": "*",
3004
                 "ext-filter": "*",
3004
                 "ext-filter": "*",
3008
                 "ext-session": "*",
3008
                 "ext-session": "*",
3009
                 "ext-tokenizer": "*",
3009
                 "ext-tokenizer": "*",
3010
                 "fruitcake/php-cors": "^1.3",
3010
                 "fruitcake/php-cors": "^1.3",
3011
-                "guzzlehttp/guzzle": "^7.8",
3011
+                "guzzlehttp/guzzle": "^7.8.2",
3012
                 "guzzlehttp/uri-template": "^1.0",
3012
                 "guzzlehttp/uri-template": "^1.0",
3013
                 "laravel/prompts": "^0.1.18|^0.2.0|^0.3.0",
3013
                 "laravel/prompts": "^0.1.18|^0.2.0|^0.3.0",
3014
                 "laravel/serializable-closure": "^1.3|^2.0",
3014
                 "laravel/serializable-closure": "^1.3|^2.0",
3015
                 "league/commonmark": "^2.2.1",
3015
                 "league/commonmark": "^2.2.1",
3016
-                "league/flysystem": "^3.8.0",
3016
+                "league/flysystem": "^3.25.1",
3017
+                "league/flysystem-local": "^3.25.1",
3017
                 "monolog/monolog": "^3.0",
3018
                 "monolog/monolog": "^3.0",
3018
-                "nesbot/carbon": "^2.72.2|^3.0",
3019
+                "nesbot/carbon": "^2.72.2|^3.4",
3019
                 "nunomaduro/termwind": "^2.0",
3020
                 "nunomaduro/termwind": "^2.0",
3020
                 "php": "^8.2",
3021
                 "php": "^8.2",
3021
                 "psr/container": "^1.1.1|^2.0.1",
3022
                 "psr/container": "^1.1.1|^2.0.1",
3022
                 "psr/log": "^1.0|^2.0|^3.0",
3023
                 "psr/log": "^1.0|^2.0|^3.0",
3023
                 "psr/simple-cache": "^1.0|^2.0|^3.0",
3024
                 "psr/simple-cache": "^1.0|^2.0|^3.0",
3024
                 "ramsey/uuid": "^4.7",
3025
                 "ramsey/uuid": "^4.7",
3025
-                "symfony/console": "^7.0",
3026
-                "symfony/error-handler": "^7.0",
3027
-                "symfony/finder": "^7.0",
3028
-                "symfony/http-foundation": "^7.0",
3029
-                "symfony/http-kernel": "^7.0",
3030
-                "symfony/mailer": "^7.0",
3031
-                "symfony/mime": "^7.0",
3032
-                "symfony/polyfill-php83": "^1.28",
3033
-                "symfony/process": "^7.0",
3034
-                "symfony/routing": "^7.0",
3035
-                "symfony/uid": "^7.0",
3036
-                "symfony/var-dumper": "^7.0",
3026
+                "symfony/console": "^7.0.3",
3027
+                "symfony/error-handler": "^7.0.3",
3028
+                "symfony/finder": "^7.0.3",
3029
+                "symfony/http-foundation": "^7.0.3",
3030
+                "symfony/http-kernel": "^7.0.3",
3031
+                "symfony/mailer": "^7.0.3",
3032
+                "symfony/mime": "^7.0.3",
3033
+                "symfony/polyfill-php83": "^1.31",
3034
+                "symfony/process": "^7.0.3",
3035
+                "symfony/routing": "^7.0.3",
3036
+                "symfony/uid": "^7.0.3",
3037
+                "symfony/var-dumper": "^7.0.3",
3037
                 "tijsverkoyen/css-to-inline-styles": "^2.2.5",
3038
                 "tijsverkoyen/css-to-inline-styles": "^2.2.5",
3038
-                "vlucas/phpdotenv": "^5.4.1",
3039
-                "voku/portable-ascii": "^2.0"
3039
+                "vlucas/phpdotenv": "^5.6.1",
3040
+                "voku/portable-ascii": "^2.0.2"
3040
             },
3041
             },
3041
             "conflict": {
3042
             "conflict": {
3042
                 "mockery/mockery": "1.6.8",
3043
                 "mockery/mockery": "1.6.8",
3086
             },
3087
             },
3087
             "require-dev": {
3088
             "require-dev": {
3088
                 "ably/ably-php": "^1.0",
3089
                 "ably/ably-php": "^1.0",
3089
-                "aws/aws-sdk-php": "^3.235.5",
3090
+                "aws/aws-sdk-php": "^3.322.9",
3090
                 "ext-gmp": "*",
3091
                 "ext-gmp": "*",
3091
-                "fakerphp/faker": "^1.23",
3092
-                "league/flysystem-aws-s3-v3": "^3.0",
3093
-                "league/flysystem-ftp": "^3.0",
3094
-                "league/flysystem-path-prefixing": "^3.3",
3095
-                "league/flysystem-read-only": "^3.3",
3096
-                "league/flysystem-sftp-v3": "^3.0",
3092
+                "fakerphp/faker": "^1.24",
3093
+                "guzzlehttp/promises": "^2.0.3",
3094
+                "guzzlehttp/psr7": "^2.4",
3095
+                "league/flysystem-aws-s3-v3": "^3.25.1",
3096
+                "league/flysystem-ftp": "^3.25.1",
3097
+                "league/flysystem-path-prefixing": "^3.25.1",
3098
+                "league/flysystem-read-only": "^3.25.1",
3099
+                "league/flysystem-sftp-v3": "^3.25.1",
3097
                 "mockery/mockery": "^1.6.10",
3100
                 "mockery/mockery": "^1.6.10",
3098
                 "nyholm/psr7": "^1.2",
3101
                 "nyholm/psr7": "^1.2",
3099
                 "orchestra/testbench-core": "^9.6",
3102
                 "orchestra/testbench-core": "^9.6",
3100
-                "pda/pheanstalk": "^5.0",
3103
+                "pda/pheanstalk": "^5.0.6",
3101
                 "phpstan/phpstan": "^1.11.5",
3104
                 "phpstan/phpstan": "^1.11.5",
3102
-                "phpunit/phpunit": "^10.5|^11.0",
3103
-                "predis/predis": "^2.0.2",
3105
+                "phpunit/phpunit": "^10.5.35|^11.3.6",
3106
+                "predis/predis": "^2.3",
3104
                 "resend/resend-php": "^0.10.0",
3107
                 "resend/resend-php": "^0.10.0",
3105
-                "symfony/cache": "^7.0",
3106
-                "symfony/http-client": "^7.0",
3107
-                "symfony/psr-http-message-bridge": "^7.0"
3108
+                "symfony/cache": "^7.0.3",
3109
+                "symfony/http-client": "^7.0.3",
3110
+                "symfony/psr-http-message-bridge": "^7.0.3",
3111
+                "symfony/translation": "^7.0.3"
3108
             },
3112
             },
3109
             "suggest": {
3113
             "suggest": {
3110
                 "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).",
3114
                 "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).",
3111
-                "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.235.5).",
3115
+                "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.322.9).",
3112
                 "brianium/paratest": "Required to run tests in parallel (^7.0|^8.0).",
3116
                 "brianium/paratest": "Required to run tests in parallel (^7.0|^8.0).",
3113
                 "ext-apcu": "Required to use the APC cache driver.",
3117
                 "ext-apcu": "Required to use the APC cache driver.",
3114
                 "ext-fileinfo": "Required to use the Filesystem class.",
3118
                 "ext-fileinfo": "Required to use the Filesystem class.",
3122
                 "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).",
3126
                 "fakerphp/faker": "Required to use the eloquent factory builder (^1.9.1).",
3123
                 "filp/whoops": "Required for friendly error pages in development (^2.14.3).",
3127
                 "filp/whoops": "Required for friendly error pages in development (^2.14.3).",
3124
                 "laravel/tinker": "Required to use the tinker console command (^2.0).",
3128
                 "laravel/tinker": "Required to use the tinker console command (^2.0).",
3125
-                "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.0).",
3126
-                "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.0).",
3127
-                "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.3).",
3128
-                "league/flysystem-read-only": "Required to use read-only disks (^3.3)",
3129
-                "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.0).",
3129
+                "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).",
3130
+                "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).",
3131
+                "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.25.1).",
3132
+                "league/flysystem-read-only": "Required to use read-only disks (^3.25.1)",
3133
+                "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).",
3130
                 "mockery/mockery": "Required to use mocking (^1.6).",
3134
                 "mockery/mockery": "Required to use mocking (^1.6).",
3131
                 "nyholm/psr7": "Required to use PSR-7 bridging features (^1.2).",
3135
                 "nyholm/psr7": "Required to use PSR-7 bridging features (^1.2).",
3132
                 "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).",
3136
                 "pda/pheanstalk": "Required to use the beanstalk queue driver (^5.0).",
3133
                 "phpunit/phpunit": "Required to use assertions and run tests (^10.5|^11.0).",
3137
                 "phpunit/phpunit": "Required to use assertions and run tests (^10.5|^11.0).",
3134
-                "predis/predis": "Required to use the predis connector (^2.0.2).",
3138
+                "predis/predis": "Required to use the predis connector (^2.3).",
3135
                 "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).",
3139
                 "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).",
3136
                 "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).",
3140
                 "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0|^7.0).",
3137
                 "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0).",
3141
                 "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0).",
3187
                 "issues": "https://github.com/laravel/framework/issues",
3191
                 "issues": "https://github.com/laravel/framework/issues",
3188
                 "source": "https://github.com/laravel/framework"
3192
                 "source": "https://github.com/laravel/framework"
3189
             },
3193
             },
3190
-            "time": "2024-11-19T22:47:13+00:00"
3194
+            "time": "2024-11-26T21:32:01+00:00"
3191
         },
3195
         },
3192
         {
3196
         {
3193
             "name": "laravel/prompts",
3197
             "name": "laravel/prompts",
3250
         },
3254
         },
3251
         {
3255
         {
3252
             "name": "laravel/sanctum",
3256
             "name": "laravel/sanctum",
3253
-            "version": "v4.0.4",
3257
+            "version": "v4.0.5",
3254
             "source": {
3258
             "source": {
3255
                 "type": "git",
3259
                 "type": "git",
3256
                 "url": "https://github.com/laravel/sanctum.git",
3260
                 "url": "https://github.com/laravel/sanctum.git",
3257
-                "reference": "819782c75aaf2b08da1765503893bd2b8023d3b3"
3261
+                "reference": "fe361b9a63407a228f884eb78d7217f680b50140"
3258
             },
3262
             },
3259
             "dist": {
3263
             "dist": {
3260
                 "type": "zip",
3264
                 "type": "zip",
3261
-                "url": "https://api.github.com/repos/laravel/sanctum/zipball/819782c75aaf2b08da1765503893bd2b8023d3b3",
3262
-                "reference": "819782c75aaf2b08da1765503893bd2b8023d3b3",
3265
+                "url": "https://api.github.com/repos/laravel/sanctum/zipball/fe361b9a63407a228f884eb78d7217f680b50140",
3266
+                "reference": "fe361b9a63407a228f884eb78d7217f680b50140",
3263
                 "shasum": ""
3267
                 "shasum": ""
3264
             },
3268
             },
3265
             "require": {
3269
             "require": {
3310
                 "issues": "https://github.com/laravel/sanctum/issues",
3314
                 "issues": "https://github.com/laravel/sanctum/issues",
3311
                 "source": "https://github.com/laravel/sanctum"
3315
                 "source": "https://github.com/laravel/sanctum"
3312
             },
3316
             },
3313
-            "time": "2024-11-15T14:47:23+00:00"
3317
+            "time": "2024-11-26T14:36:23+00:00"
3314
         },
3318
         },
3315
         {
3319
         {
3316
             "name": "laravel/serializable-closure",
3320
             "name": "laravel/serializable-closure",
9632
         },
9636
         },
9633
         {
9637
         {
9634
             "name": "laravel/pint",
9638
             "name": "laravel/pint",
9635
-            "version": "v1.18.2",
9639
+            "version": "v1.18.3",
9636
             "source": {
9640
             "source": {
9637
                 "type": "git",
9641
                 "type": "git",
9638
                 "url": "https://github.com/laravel/pint.git",
9642
                 "url": "https://github.com/laravel/pint.git",
9639
-                "reference": "f55daaf7eb6c2f49ddf6702fb42e3091c64d8a64"
9643
+                "reference": "cef51821608239040ab841ad6e1c6ae502ae3026"
9640
             },
9644
             },
9641
             "dist": {
9645
             "dist": {
9642
                 "type": "zip",
9646
                 "type": "zip",
9643
-                "url": "https://api.github.com/repos/laravel/pint/zipball/f55daaf7eb6c2f49ddf6702fb42e3091c64d8a64",
9644
-                "reference": "f55daaf7eb6c2f49ddf6702fb42e3091c64d8a64",
9647
+                "url": "https://api.github.com/repos/laravel/pint/zipball/cef51821608239040ab841ad6e1c6ae502ae3026",
9648
+                "reference": "cef51821608239040ab841ad6e1c6ae502ae3026",
9645
                 "shasum": ""
9649
                 "shasum": ""
9646
             },
9650
             },
9647
             "require": {
9651
             "require": {
9652
                 "php": "^8.1.0"
9656
                 "php": "^8.1.0"
9653
             },
9657
             },
9654
             "require-dev": {
9658
             "require-dev": {
9655
-                "friendsofphp/php-cs-fixer": "^3.64.0",
9656
-                "illuminate/view": "^10.48.20",
9657
-                "larastan/larastan": "^2.9.8",
9659
+                "friendsofphp/php-cs-fixer": "^3.65.0",
9660
+                "illuminate/view": "^10.48.24",
9661
+                "larastan/larastan": "^2.9.11",
9658
                 "laravel-zero/framework": "^10.4.0",
9662
                 "laravel-zero/framework": "^10.4.0",
9659
                 "mockery/mockery": "^1.6.12",
9663
                 "mockery/mockery": "^1.6.12",
9660
-                "nunomaduro/termwind": "^1.15.1",
9661
-                "pestphp/pest": "^2.35.1"
9664
+                "nunomaduro/termwind": "^1.17.0",
9665
+                "pestphp/pest": "^2.36.0"
9662
             },
9666
             },
9663
             "bin": [
9667
             "bin": [
9664
                 "builds/pint"
9668
                 "builds/pint"
9694
                 "issues": "https://github.com/laravel/pint/issues",
9698
                 "issues": "https://github.com/laravel/pint/issues",
9695
                 "source": "https://github.com/laravel/pint"
9699
                 "source": "https://github.com/laravel/pint"
9696
             },
9700
             },
9697
-            "time": "2024-11-20T09:33:46+00:00"
9701
+            "time": "2024-11-26T15:34:00+00:00"
9698
         },
9702
         },
9699
         {
9703
         {
9700
             "name": "laravel/sail",
9704
             "name": "laravel/sail",
9701
-            "version": "v1.38.0",
9705
+            "version": "v1.39.0",
9702
             "source": {
9706
             "source": {
9703
                 "type": "git",
9707
                 "type": "git",
9704
                 "url": "https://github.com/laravel/sail.git",
9708
                 "url": "https://github.com/laravel/sail.git",
9705
-                "reference": "d17abae06661dd6c46d13627b1683a2924259145"
9709
+                "reference": "be9d67a11133535811f9ec4ab5c176a2f47250fc"
9706
             },
9710
             },
9707
             "dist": {
9711
             "dist": {
9708
                 "type": "zip",
9712
                 "type": "zip",
9709
-                "url": "https://api.github.com/repos/laravel/sail/zipball/d17abae06661dd6c46d13627b1683a2924259145",
9710
-                "reference": "d17abae06661dd6c46d13627b1683a2924259145",
9713
+                "url": "https://api.github.com/repos/laravel/sail/zipball/be9d67a11133535811f9ec4ab5c176a2f47250fc",
9714
+                "reference": "be9d67a11133535811f9ec4ab5c176a2f47250fc",
9711
                 "shasum": ""
9715
                 "shasum": ""
9712
             },
9716
             },
9713
             "require": {
9717
             "require": {
9757
                 "issues": "https://github.com/laravel/sail/issues",
9761
                 "issues": "https://github.com/laravel/sail/issues",
9758
                 "source": "https://github.com/laravel/sail"
9762
                 "source": "https://github.com/laravel/sail"
9759
             },
9763
             },
9760
-            "time": "2024-11-11T20:16:51+00:00"
9764
+            "time": "2024-11-25T23:48:26+00:00"
9761
         },
9765
         },
9762
         {
9766
         {
9763
             "name": "mockery/mockery",
9767
             "name": "mockery/mockery",

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

1707
             }
1707
             }
1708
         },
1708
         },
1709
         "node_modules/nanoid": {
1709
         "node_modules/nanoid": {
1710
-            "version": "3.3.7",
1711
-            "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
1712
-            "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
1710
+            "version": "3.3.8",
1711
+            "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
1712
+            "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
1713
             "dev": true,
1713
             "dev": true,
1714
             "funding": [
1714
             "funding": [
1715
                 {
1715
                 {

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

17
                 <td class="w-2/3 text-sm px-4 py-2 font-medium leading-6 text-gray-950 dark:text-white">Taxes:</td>
17
                 <td class="w-2/3 text-sm px-4 py-2 font-medium leading-6 text-gray-950 dark:text-white">Taxes:</td>
18
                 <td class="w-1/3 text-sm pl-4 py-2 leading-6">{{ $taxTotal }}</td>
18
                 <td class="w-1/3 text-sm pl-4 py-2 leading-6">{{ $taxTotal }}</td>
19
             </tr>
19
             </tr>
20
+            <tr>
21
+                <td class="w-2/3 text-sm px-4 py-2 font-medium leading-6 text-gray-950 dark:text-white">Discounts:</td>
22
+                <td class="w-1/3 text-sm pl-4 py-2 leading-6">({{ $discountTotal }})</td>
23
+            </tr>
20
             <tr class="font-semibold">
24
             <tr class="font-semibold">
21
                 <td class="w-2/3 text-sm px-4 py-2 font-medium leading-6 text-gray-950 dark:text-white">Total:</td>
25
                 <td class="w-2/3 text-sm px-4 py-2 font-medium leading-6 text-gray-950 dark:text-white">Total:</td>
22
                 <td class="w-1/3 text-sm pl-4 py-2 leading-6">{{ $grandTotal }}</td>
26
                 <td class="w-1/3 text-sm pl-4 py-2 leading-6">{{ $grandTotal }}</td>

Loading…
取消
儲存