瀏覽代碼

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

Development 3.x
3.x
Andrew Wallo 10 月之前
父節點
當前提交
755a322527
沒有連結到貢獻者的電子郵件帳戶。
共有 41 個檔案被更改,包括 1330 行新增1000 行删除
  1. 119
    0
      app/Concerns/ManagesLineItems.php
  2. 13
    0
      app/Enums/Accounting/AdjustmentComputation.php
  3. 32
    0
      app/Enums/Accounting/DocumentDiscountMethod.php
  4. 0
    73
      app/Filament/Company/Clusters/Settings/Pages/CompanyDefault.php
  5. 2
    2
      app/Filament/Company/Clusters/Settings/Resources/AdjustmentResource.php
  6. 52
    125
      app/Filament/Company/Resources/Purchases/BillResource.php
  7. 22
    0
      app/Filament/Company/Resources/Purchases/BillResource/Pages/CreateBill.php
  8. 28
    0
      app/Filament/Company/Resources/Purchases/BillResource/Pages/EditBill.php
  9. 52
    121
      app/Filament/Company/Resources/Sales/InvoiceResource.php
  10. 18
    0
      app/Filament/Company/Resources/Sales/InvoiceResource/Pages/CreateInvoice.php
  11. 26
    0
      app/Filament/Company/Resources/Sales/InvoiceResource/Pages/EditInvoice.php
  12. 51
    33
      app/Filament/Company/Resources/Sales/InvoiceResource/Pages/ViewInvoice.php
  13. 2
    0
      app/Filament/Company/Resources/Sales/InvoiceResource/RelationManagers/PaymentsRelationManager.php
  14. 3
    1
      app/Filament/Company/Resources/Sales/InvoiceResource/Widgets/InvoiceOverview.php
  15. 37
    0
      app/Filament/Forms/Components/BillTotals.php
  16. 37
    0
      app/Filament/Forms/Components/InvoiceTotals.php
  17. 10
    0
      app/Models/Accounting/Account.php
  18. 38
    1
      app/Models/Accounting/Bill.php
  19. 38
    1
      app/Models/Accounting/Invoice.php
  20. 0
    82
      app/Models/Setting/Discount.php
  21. 0
    78
      app/Models/Setting/Tax.php
  22. 0
    2
      app/Providers/AuthServiceProvider.php
  23. 16
    13
      app/Providers/MacroServiceProvider.php
  24. 11
    2
      app/Services/ReportService.php
  25. 27
    11
      app/View/Models/BillTotalViewModel.php
  26. 27
    11
      app/View/Models/InvoiceTotalViewModel.php
  27. 1
    1
      composer.json
  28. 313
    376
      composer.lock
  29. 3
    1
      database/factories/Accounting/AdjustmentFactory.php
  30. 3
    0
      database/migrations/2024_11_27_221657_create_bills_table.php
  31. 3
    0
      database/migrations/2024_11_27_223015_create_invoices_table.php
  32. 25
    25
      package-lock.json
  33. 11
    1
      resources/views/components/company/invoice/container.blade.php
  34. 1
    1
      resources/views/components/company/invoice/footer.blade.php
  35. 1
    1
      resources/views/filament/company/components/invoice-layouts/classic.blade.php
  36. 1
    1
      resources/views/filament/company/components/invoice-layouts/default.blade.php
  37. 1
    1
      resources/views/filament/company/components/invoice-layouts/modern.blade.php
  38. 169
    0
      resources/views/filament/company/resources/sales/invoices/components/invoice-view.blade.php
  39. 47
    0
      resources/views/filament/company/resources/sales/invoices/pages/view-invoice.blade.php
  40. 45
    18
      resources/views/filament/forms/components/bill-totals.blade.php
  41. 45
    18
      resources/views/filament/forms/components/invoice-totals.blade.php

+ 119
- 0
app/Concerns/ManagesLineItems.php 查看文件

1
+<?php
2
+
3
+namespace App\Concerns;
4
+
5
+use App\Enums\Accounting\AdjustmentComputation;
6
+use App\Enums\Accounting\DocumentDiscountMethod;
7
+use App\Models\Accounting\Bill;
8
+use App\Models\Accounting\DocumentLineItem;
9
+use App\Utilities\Currency\CurrencyConverter;
10
+use Illuminate\Database\Eloquent\Model;
11
+use Illuminate\Support\Collection;
12
+
13
+trait ManagesLineItems
14
+{
15
+    protected function handleLineItems(Model $record, Collection $lineItems): void
16
+    {
17
+        foreach ($lineItems as $itemData) {
18
+            $lineItem = isset($itemData['id'])
19
+                ? $record->lineItems->find($itemData['id'])
20
+                : $record->lineItems()->make();
21
+
22
+            $lineItem->fill([
23
+                'offering_id' => $itemData['offering_id'],
24
+                'description' => $itemData['description'],
25
+                'quantity' => $itemData['quantity'],
26
+                'unit_price' => $itemData['unit_price'],
27
+            ]);
28
+
29
+            if (! $lineItem->exists) {
30
+                $lineItem->documentable()->associate($record);
31
+            }
32
+
33
+            $lineItem->save();
34
+
35
+            $this->handleLineItemAdjustments($lineItem, $itemData, $record->discount_method);
36
+            $this->updateLineItemTotals($lineItem, $record->discount_method);
37
+        }
38
+    }
39
+
40
+    protected function deleteRemovedLineItems(Model $record, Collection $lineItems): void
41
+    {
42
+        $existingLineItemIds = $record->lineItems->pluck('id');
43
+        $updatedLineItemIds = $lineItems->pluck('id')->filter();
44
+        $lineItemsToDelete = $existingLineItemIds->diff($updatedLineItemIds);
45
+
46
+        if ($lineItemsToDelete->isNotEmpty()) {
47
+            $record
48
+                ->lineItems()
49
+                ->whereIn('id', $lineItemsToDelete)
50
+                ->each(fn (DocumentLineItem $lineItem) => $lineItem->delete());
51
+        }
52
+    }
53
+
54
+    protected function handleLineItemAdjustments(DocumentLineItem $lineItem, array $itemData, DocumentDiscountMethod $discountMethod): void
55
+    {
56
+        $isBill = $lineItem->documentable instanceof Bill;
57
+
58
+        $taxType = $isBill ? 'purchaseTaxes' : 'salesTaxes';
59
+        $discountType = $isBill ? 'purchaseDiscounts' : 'salesDiscounts';
60
+
61
+        $adjustmentIds = collect($itemData[$taxType] ?? [])
62
+            ->merge($discountMethod->isPerLineItem() ? ($itemData[$discountType] ?? []) : [])
63
+            ->filter()
64
+            ->unique();
65
+
66
+        $lineItem->adjustments()->sync($adjustmentIds);
67
+        $lineItem->refresh();
68
+    }
69
+
70
+    protected function updateLineItemTotals(DocumentLineItem $lineItem, DocumentDiscountMethod $discountMethod): void
71
+    {
72
+        $lineItem->updateQuietly([
73
+            'tax_total' => $lineItem->calculateTaxTotal()->getAmount(),
74
+            'discount_total' => $discountMethod->isPerLineItem()
75
+                ? $lineItem->calculateDiscountTotal()->getAmount()
76
+                : 0,
77
+        ]);
78
+    }
79
+
80
+    protected function updateDocumentTotals(Model $record, array $data): array
81
+    {
82
+        $subtotalCents = $record->lineItems()->sum('subtotal');
83
+        $taxTotalCents = $record->lineItems()->sum('tax_total');
84
+        $discountTotalCents = $this->calculateDiscountTotal(
85
+            DocumentDiscountMethod::parse($data['discount_method']),
86
+            AdjustmentComputation::parse($data['discount_computation']),
87
+            $data['discount_rate'] ?? null,
88
+            $subtotalCents,
89
+            $record
90
+        );
91
+
92
+        $grandTotalCents = $subtotalCents + $taxTotalCents - $discountTotalCents;
93
+
94
+        return [
95
+            'subtotal' => CurrencyConverter::convertCentsToFormatSimple($subtotalCents),
96
+            'tax_total' => CurrencyConverter::convertCentsToFormatSimple($taxTotalCents),
97
+            'discount_total' => CurrencyConverter::convertCentsToFormatSimple($discountTotalCents),
98
+            'total' => CurrencyConverter::convertCentsToFormatSimple($grandTotalCents),
99
+        ];
100
+    }
101
+
102
+    protected function calculateDiscountTotal(
103
+        DocumentDiscountMethod $discountMethod,
104
+        ?AdjustmentComputation $discountComputation,
105
+        ?string $discountRate,
106
+        int $subtotalCents,
107
+        Model $record
108
+    ): int {
109
+        if ($discountMethod->isPerLineItem()) {
110
+            return $record->lineItems()->sum('discount_total');
111
+        }
112
+
113
+        if ($discountComputation?->isPercentage()) {
114
+            return (int) ($subtotalCents * ((float) $discountRate / 100));
115
+        }
116
+
117
+        return CurrencyConverter::convertToCents($discountRate);
118
+    }
119
+}

+ 13
- 0
app/Enums/Accounting/AdjustmentComputation.php 查看文件

2
 
2
 
3
 namespace App\Enums\Accounting;
3
 namespace App\Enums\Accounting;
4
 
4
 
5
+use App\Enums\Concerns\ParsesEnum;
5
 use Filament\Support\Contracts\HasLabel;
6
 use Filament\Support\Contracts\HasLabel;
6
 
7
 
7
 enum AdjustmentComputation: string implements HasLabel
8
 enum AdjustmentComputation: string implements HasLabel
8
 {
9
 {
10
+    use ParsesEnum;
11
+
9
     case Percentage = 'percentage';
12
     case Percentage = 'percentage';
10
     case Fixed = 'fixed';
13
     case Fixed = 'fixed';
11
 
14
 
13
     {
16
     {
14
         return translate($this->name);
17
         return translate($this->name);
15
     }
18
     }
19
+
20
+    public function isPercentage(): bool
21
+    {
22
+        return $this == self::Percentage;
23
+    }
24
+
25
+    public function isFixed(): bool
26
+    {
27
+        return $this == self::Fixed;
28
+    }
16
 }
29
 }

+ 32
- 0
app/Enums/Accounting/DocumentDiscountMethod.php 查看文件

1
+<?php
2
+
3
+namespace App\Enums\Accounting;
4
+
5
+use App\Enums\Concerns\ParsesEnum;
6
+use Filament\Support\Contracts\HasLabel;
7
+
8
+enum DocumentDiscountMethod: string implements HasLabel
9
+{
10
+    use ParsesEnum;
11
+
12
+    case PerLineItem = 'per_line_item';
13
+    case PerDocument = 'per_document';
14
+
15
+    public function getLabel(): string
16
+    {
17
+        return match ($this) {
18
+            self::PerLineItem => 'Per Line Item',
19
+            self::PerDocument => 'Per Document',
20
+        };
21
+    }
22
+
23
+    public function isPerLineItem(): bool
24
+    {
25
+        return $this == self::PerLineItem;
26
+    }
27
+
28
+    public function isPerDocument(): bool
29
+    {
30
+        return $this == self::PerDocument;
31
+    }
32
+}

+ 0
- 73
app/Filament/Company/Clusters/Settings/Pages/CompanyDefault.php 查看文件

6
 use App\Filament\Company\Clusters\Settings;
6
 use App\Filament\Company\Clusters\Settings;
7
 use App\Models\Banking\BankAccount;
7
 use App\Models\Banking\BankAccount;
8
 use App\Models\Setting\CompanyDefault as CompanyDefaultModel;
8
 use App\Models\Setting\CompanyDefault as CompanyDefaultModel;
9
-use App\Models\Setting\Discount;
10
-use App\Models\Setting\Tax;
11
 use Filament\Actions\Action;
9
 use Filament\Actions\Action;
12
 use Filament\Actions\ActionGroup;
10
 use Filament\Actions\ActionGroup;
13
 use Filament\Forms\Components\Component;
11
 use Filament\Forms\Components\Component;
108
         return $form
106
         return $form
109
             ->schema([
107
             ->schema([
110
                 $this->getGeneralSection(),
108
                 $this->getGeneralSection(),
111
-                // $this->getModifiersSection(),
112
             ])
109
             ])
113
             ->model($this->record)
110
             ->model($this->record)
114
             ->statePath('data')
111
             ->statePath('data')
140
             ])->columns();
137
             ])->columns();
141
     }
138
     }
142
 
139
 
143
-    protected function getModifiersSection(): Component
144
-    {
145
-        return Section::make('Taxes & Discounts')
146
-            ->schema([
147
-                Select::make('sales_tax_id')
148
-                    ->softRequired()
149
-                    ->localizeLabel()
150
-                    ->relationship('salesTax', 'name')
151
-                    ->getOptionLabelFromRecordUsing(function (Tax $record) {
152
-                        $currencyCode = $this->record->currency_code;
153
-
154
-                        $rate = rateFormat($record->rate, $record->computation->value, $currencyCode);
155
-
156
-                        $rateBadge = $this->renderBadgeOptionLabel($rate);
157
-
158
-                        return "{$record->name} ⁓ {$rateBadge}";
159
-                    })
160
-                    ->allowHtml()
161
-                    ->saveRelationshipsUsing(null)
162
-                    ->searchable(),
163
-                Select::make('purchase_tax_id')
164
-                    ->softRequired()
165
-                    ->localizeLabel()
166
-                    ->relationship('purchaseTax', 'name')
167
-                    ->getOptionLabelFromRecordUsing(function (Tax $record) {
168
-                        $currencyCode = $this->record->currency_code;
169
-
170
-                        $rate = rateFormat($record->rate, $record->computation->value, $currencyCode);
171
-
172
-                        $rateBadge = $this->renderBadgeOptionLabel($rate);
173
-
174
-                        return "{$record->name} ⁓ {$rateBadge}";
175
-                    })
176
-                    ->allowHtml()
177
-                    ->saveRelationshipsUsing(null)
178
-                    ->searchable(),
179
-                Select::make('sales_discount_id')
180
-                    ->softRequired()
181
-                    ->localizeLabel()
182
-                    ->relationship('salesDiscount', 'name')
183
-                    ->getOptionLabelFromRecordUsing(function (Discount $record) {
184
-                        $currencyCode = $this->record->currency_code;
185
-
186
-                        $rate = rateFormat($record->rate, $record->computation->value, $currencyCode);
187
-
188
-                        $rateBadge = $this->renderBadgeOptionLabel($rate);
189
-
190
-                        return "{$record->name} ⁓ {$rateBadge}";
191
-                    })
192
-                    ->saveRelationshipsUsing(null)
193
-                    ->allowHtml()
194
-                    ->searchable(),
195
-                Select::make('purchase_discount_id')
196
-                    ->softRequired()
197
-                    ->localizeLabel()
198
-                    ->relationship('purchaseDiscount', 'name')
199
-                    ->getOptionLabelFromRecordUsing(function (Discount $record) {
200
-                        $currencyCode = $this->record->currency_code;
201
-                        $rate = rateFormat($record->rate, $record->computation->value, $currencyCode);
202
-
203
-                        $rateBadge = $this->renderBadgeOptionLabel($rate);
204
-
205
-                        return "{$record->name} ⁓ {$rateBadge}";
206
-                    })
207
-                    ->allowHtml()
208
-                    ->saveRelationshipsUsing(null)
209
-                    ->searchable(),
210
-            ])->columns();
211
-    }
212
-
213
     public function renderBadgeOptionLabel(string $label): string
140
     public function renderBadgeOptionLabel(string $label): string
214
     {
141
     {
215
         return Blade::render('filament::components.badge', [
142
         return Blade::render('filament::components.badge', [

+ 2
- 2
app/Filament/Company/Clusters/Settings/Resources/AdjustmentResource.php 查看文件

53
                         ToggleButton::make('recoverable')
53
                         ToggleButton::make('recoverable')
54
                             ->label('Recoverable')
54
                             ->label('Recoverable')
55
                             ->default(false)
55
                             ->default(false)
56
-                            ->visible(fn (Forms\Get $get) => AdjustmentCategory::parse($get('category')) === AdjustmentCategory::Tax && AdjustmentType::parse($get('type')) === AdjustmentType::Purchase),
56
+                            ->visible(fn (Forms\Get $get) => AdjustmentCategory::parse($get('category'))->isTax() && AdjustmentType::parse($get('type'))->isPurchase()),
57
                     ])
57
                     ])
58
                     ->columns()
58
                     ->columns()
59
                     ->visibleOn('create'),
59
                     ->visibleOn('create'),
80
                         Forms\Components\DateTimePicker::make('end_date'),
80
                         Forms\Components\DateTimePicker::make('end_date'),
81
                     ])
81
                     ])
82
                     ->columns()
82
                     ->columns()
83
-                    ->visible(fn (Forms\Get $get) => AdjustmentCategory::parse($get('category')) === AdjustmentCategory::Discount),
83
+                    ->visible(fn (Forms\Get $get) => AdjustmentCategory::parse($get('category'))->isDiscount()),
84
             ]);
84
             ]);
85
     }
85
     }
86
 
86
 

+ 52
- 125
app/Filament/Company/Resources/Purchases/BillResource.php 查看文件

3
 namespace App\Filament\Company\Resources\Purchases;
3
 namespace App\Filament\Company\Resources\Purchases;
4
 
4
 
5
 use App\Enums\Accounting\BillStatus;
5
 use App\Enums\Accounting\BillStatus;
6
+use App\Enums\Accounting\DocumentDiscountMethod;
6
 use App\Enums\Accounting\PaymentMethod;
7
 use App\Enums\Accounting\PaymentMethod;
7
 use App\Filament\Company\Resources\Purchases\BillResource\Pages;
8
 use App\Filament\Company\Resources\Purchases\BillResource\Pages;
9
+use App\Filament\Forms\Components\BillTotals;
8
 use App\Filament\Tables\Actions\ReplicateBulkAction;
10
 use App\Filament\Tables\Actions\ReplicateBulkAction;
9
 use App\Filament\Tables\Filters\DateRangeFilter;
11
 use App\Filament\Tables\Filters\DateRangeFilter;
10
 use App\Models\Accounting\Adjustment;
12
 use App\Models\Accounting\Adjustment;
11
 use App\Models\Accounting\Bill;
13
 use App\Models\Accounting\Bill;
12
-use App\Models\Accounting\DocumentLineItem;
13
 use App\Models\Banking\BankAccount;
14
 use App\Models\Banking\BankAccount;
14
 use App\Models\Common\Offering;
15
 use App\Models\Common\Offering;
15
 use App\Utilities\Currency\CurrencyConverter;
16
 use App\Utilities\Currency\CurrencyConverter;
26
 use Filament\Tables\Table;
27
 use Filament\Tables\Table;
27
 use Illuminate\Database\Eloquent\Builder;
28
 use Illuminate\Database\Eloquent\Builder;
28
 use Illuminate\Database\Eloquent\Collection;
29
 use Illuminate\Database\Eloquent\Collection;
29
-use Illuminate\Database\Eloquent\Model;
30
 use Illuminate\Support\Facades\Auth;
30
 use Illuminate\Support\Facades\Auth;
31
 
31
 
32
 class BillResource extends Resource
32
 class BillResource extends Resource
71
                                         return now()->addDays($company->defaultBill->payment_terms->getDays());
71
                                         return now()->addDays($company->defaultBill->payment_terms->getDays());
72
                                     })
72
                                     })
73
                                     ->required(),
73
                                     ->required(),
74
+                                Forms\Components\Select::make('discount_method')
75
+                                    ->label('Discount Method')
76
+                                    ->options(DocumentDiscountMethod::class)
77
+                                    ->selectablePlaceholder(false)
78
+                                    ->default(DocumentDiscountMethod::PerLineItem)
79
+                                    ->afterStateUpdated(function ($state, Forms\Set $set) {
80
+                                        $discountMethod = DocumentDiscountMethod::parse($state);
81
+
82
+                                        if ($discountMethod->isPerDocument()) {
83
+                                            $set('lineItems.*.purchaseDiscounts', []);
84
+                                        }
85
+                                    })
86
+                                    ->live(),
74
                             ])->grow(true),
87
                             ])->grow(true),
75
                         ])->from('md'),
88
                         ])->from('md'),
76
                         TableRepeater::make('lineItems')
89
                         TableRepeater::make('lineItems')
77
                             ->relationship()
90
                             ->relationship()
78
-                            ->saveRelationshipsUsing(function (TableRepeater $component, Forms\Contracts\HasForms $livewire, ?array $state) {
79
-                                if (! is_array($state)) {
80
-                                    $state = [];
81
-                                }
82
-
83
-                                $relationship = $component->getRelationship();
84
-
85
-                                $existingRecords = $component->getCachedExistingRecords();
86
-
87
-                                $recordsToDelete = [];
88
-
89
-                                foreach ($existingRecords->pluck($relationship->getRelated()->getKeyName()) as $keyToCheckForDeletion) {
90
-                                    if (array_key_exists("record-{$keyToCheckForDeletion}", $state)) {
91
-                                        continue;
92
-                                    }
93
-
94
-                                    $recordsToDelete[] = $keyToCheckForDeletion;
95
-                                    $existingRecords->forget("record-{$keyToCheckForDeletion}");
91
+                            ->saveRelationshipsUsing(null)
92
+                            ->dehydrated(true)
93
+                            ->headers(function (Forms\Get $get) {
94
+                                $hasDiscounts = DocumentDiscountMethod::parse($get('discount_method'))->isPerLineItem();
95
+
96
+                                $headers = [
97
+                                    Header::make('Items')->width($hasDiscounts ? '15%' : '20%'),
98
+                                    Header::make('Description')->width($hasDiscounts ? '25%' : '30%'),  // Increase when no discounts
99
+                                    Header::make('Quantity')->width('10%'),
100
+                                    Header::make('Price')->width('10%'),
101
+                                    Header::make('Taxes')->width($hasDiscounts ? '15%' : '20%'),       // Increase when no discounts
102
+                                ];
103
+
104
+                                if ($hasDiscounts) {
105
+                                    $headers[] = Header::make('Discounts')->width('15%');
96
                                 }
106
                                 }
97
 
107
 
98
-                                $relationship
99
-                                    ->whereKey($recordsToDelete)
100
-                                    ->get()
101
-                                    ->each(static fn (Model $record) => $record->delete());
102
-
103
-                                $childComponentContainers = $component->getChildComponentContainers(
104
-                                    withHidden: $component->shouldSaveRelationshipsWhenHidden(),
105
-                                );
106
-
107
-                                $itemOrder = 1;
108
-                                $orderColumn = $component->getOrderColumn();
109
-
110
-                                $translatableContentDriver = $livewire->makeFilamentTranslatableContentDriver();
111
-
112
-                                foreach ($childComponentContainers as $itemKey => $item) {
113
-                                    $itemData = $item->getState(shouldCallHooksBefore: false);
114
-
115
-                                    if ($orderColumn) {
116
-                                        $itemData[$orderColumn] = $itemOrder;
117
-
118
-                                        $itemOrder++;
119
-                                    }
120
-
121
-                                    if ($record = ($existingRecords[$itemKey] ?? null)) {
122
-                                        $itemData = $component->mutateRelationshipDataBeforeSave($itemData, record: $record);
123
-
124
-                                        if ($itemData === null) {
125
-                                            continue;
126
-                                        }
127
-
128
-                                        $translatableContentDriver ?
129
-                                            $translatableContentDriver->updateRecord($record, $itemData) :
130
-                                            $record->fill($itemData)->save();
131
-
132
-                                        continue;
133
-                                    }
134
-
135
-                                    $relatedModel = $component->getRelatedModel();
136
-
137
-                                    $itemData = $component->mutateRelationshipDataBeforeCreate($itemData);
138
-
139
-                                    if ($itemData === null) {
140
-                                        continue;
141
-                                    }
142
-
143
-                                    if ($translatableContentDriver) {
144
-                                        $record = $translatableContentDriver->makeRecord($relatedModel, $itemData);
145
-                                    } else {
146
-                                        $record = new $relatedModel;
147
-                                        $record->fill($itemData);
148
-                                    }
108
+                                $headers[] = Header::make('Amount')->width('10%')->align('right');
149
 
109
 
150
-                                    $record = $relationship->save($record);
151
-                                    $item->model($record)->saveRelationships();
152
-                                    $existingRecords->push($record);
153
-                                }
154
-
155
-                                $component->getRecord()->setRelation($component->getRelationshipName(), $existingRecords);
156
-
157
-                                /** @var Bill $bill */
158
-                                $bill = $component->getRecord();
159
-
160
-                                // Recalculate totals for line items
161
-                                $bill->lineItems()->each(function (DocumentLineItem $lineItem) {
162
-                                    $lineItem->updateQuietly([
163
-                                        'tax_total' => $lineItem->calculateTaxTotal()->getAmount(),
164
-                                        'discount_total' => $lineItem->calculateDiscountTotal()->getAmount(),
165
-                                    ]);
166
-                                });
167
-
168
-                                $subtotal = $bill->lineItems()->sum('subtotal') / 100;
169
-                                $taxTotal = $bill->lineItems()->sum('tax_total') / 100;
170
-                                $discountTotal = $bill->lineItems()->sum('discount_total') / 100;
171
-                                $grandTotal = $subtotal + $taxTotal - $discountTotal;
172
-
173
-                                $bill->updateQuietly([
174
-                                    'subtotal' => $subtotal,
175
-                                    'tax_total' => $taxTotal,
176
-                                    'discount_total' => $discountTotal,
177
-                                    'total' => $grandTotal,
178
-                                ]);
179
-
180
-                                $bill->refresh();
181
-
182
-                                if (! $bill->initialTransaction) {
183
-                                    $bill->createInitialTransaction();
184
-                                } else {
185
-                                    $bill->updateInitialTransaction();
186
-                                }
110
+                                return $headers;
187
                             })
111
                             })
188
-                            ->headers([
189
-                                Header::make('Items')->width('15%'),
190
-                                Header::make('Description')->width('25%'),
191
-                                Header::make('Quantity')->width('10%'),
192
-                                Header::make('Price')->width('10%'),
193
-                                Header::make('Taxes')->width('15%'),
194
-                                Header::make('Discounts')->width('15%'),
195
-                                Header::make('Amount')->width('10%')->align('right'),
196
-                            ])
197
                             ->schema([
112
                             ->schema([
198
                                 Forms\Components\Select::make('offering_id')
113
                                 Forms\Components\Select::make('offering_id')
199
                                     ->relationship('purchasableOffering', 'name')
114
                                     ->relationship('purchasableOffering', 'name')
209
                                             $set('description', $offeringRecord->description);
124
                                             $set('description', $offeringRecord->description);
210
                                             $set('unit_price', $offeringRecord->price);
125
                                             $set('unit_price', $offeringRecord->price);
211
                                             $set('purchaseTaxes', $offeringRecord->purchaseTaxes->pluck('id')->toArray());
126
                                             $set('purchaseTaxes', $offeringRecord->purchaseTaxes->pluck('id')->toArray());
212
-                                            $set('purchaseDiscounts', $offeringRecord->purchaseDiscounts->pluck('id')->toArray());
127
+
128
+                                            $discountMethod = DocumentDiscountMethod::parse($get('../../discount_method'));
129
+                                            if ($discountMethod->isPerLineItem()) {
130
+                                                $set('purchaseDiscounts', $offeringRecord->purchaseDiscounts->pluck('id')->toArray());
131
+                                            }
213
                                         }
132
                                         }
214
                                     }),
133
                                     }),
215
                                 Forms\Components\TextInput::make('description'),
134
                                 Forms\Components\TextInput::make('description'),
225
                                     ->default(0),
144
                                     ->default(0),
226
                                 Forms\Components\Select::make('purchaseTaxes')
145
                                 Forms\Components\Select::make('purchaseTaxes')
227
                                     ->relationship('purchaseTaxes', 'name')
146
                                     ->relationship('purchaseTaxes', 'name')
147
+                                    ->saveRelationshipsUsing(null)
148
+                                    ->dehydrated(true)
228
                                     ->preload()
149
                                     ->preload()
229
                                     ->multiple()
150
                                     ->multiple()
230
                                     ->live()
151
                                     ->live()
231
                                     ->searchable(),
152
                                     ->searchable(),
232
                                 Forms\Components\Select::make('purchaseDiscounts')
153
                                 Forms\Components\Select::make('purchaseDiscounts')
233
                                     ->relationship('purchaseDiscounts', 'name')
154
                                     ->relationship('purchaseDiscounts', 'name')
155
+                                    ->saveRelationshipsUsing(null)
156
+                                    ->dehydrated(true)
234
                                     ->preload()
157
                                     ->preload()
235
                                     ->multiple()
158
                                     ->multiple()
236
                                     ->live()
159
                                     ->live()
160
+                                    ->hidden(function (Forms\Get $get) {
161
+                                        $discountMethod = DocumentDiscountMethod::parse($get('../../discount_method'));
162
+
163
+                                        return $discountMethod->isPerDocument();
164
+                                    })
237
                                     ->searchable(),
165
                                     ->searchable(),
238
                                 Forms\Components\Placeholder::make('total')
166
                                 Forms\Components\Placeholder::make('total')
239
                                     ->hiddenLabel()
167
                                     ->hiddenLabel()
265
                                         return CurrencyConverter::formatToMoney($total);
193
                                         return CurrencyConverter::formatToMoney($total);
266
                                     }),
194
                                     }),
267
                             ]),
195
                             ]),
268
-                        Forms\Components\Grid::make(6)
269
-                            ->schema([
270
-                                Forms\Components\ViewField::make('totals')
271
-                                    ->columnStart(5)
272
-                                    ->columnSpan(2)
273
-                                    ->view('filament.forms.components.bill-totals'),
274
-                            ]),
196
+                        BillTotals::make(),
275
                     ]),
197
                     ]),
276
             ]);
198
             ]);
277
     }
199
     }
281
         return $table
203
         return $table
282
             ->defaultSort('due_date')
204
             ->defaultSort('due_date')
283
             ->columns([
205
             ->columns([
206
+                Tables\Columns\TextColumn::make('id')
207
+                    ->label('ID')
208
+                    ->sortable()
209
+                    ->toggleable(isToggledHiddenByDefault: true)
210
+                    ->searchable(),
284
                 Tables\Columns\TextColumn::make('status')
211
                 Tables\Columns\TextColumn::make('status')
285
                     ->badge()
212
                     ->badge()
286
                     ->searchable(),
213
                     ->searchable(),

+ 22
- 0
app/Filament/Company/Resources/Purchases/BillResource/Pages/CreateBill.php 查看文件

2
 
2
 
3
 namespace App\Filament\Company\Resources\Purchases\BillResource\Pages;
3
 namespace App\Filament\Company\Resources\Purchases\BillResource\Pages;
4
 
4
 
5
+use App\Concerns\ManagesLineItems;
5
 use App\Concerns\RedirectToListPage;
6
 use App\Concerns\RedirectToListPage;
6
 use App\Filament\Company\Resources\Purchases\BillResource;
7
 use App\Filament\Company\Resources\Purchases\BillResource;
8
+use App\Models\Accounting\Bill;
7
 use Filament\Resources\Pages\CreateRecord;
9
 use Filament\Resources\Pages\CreateRecord;
8
 use Filament\Support\Enums\MaxWidth;
10
 use Filament\Support\Enums\MaxWidth;
11
+use Illuminate\Database\Eloquent\Model;
9
 
12
 
10
 class CreateBill extends CreateRecord
13
 class CreateBill extends CreateRecord
11
 {
14
 {
15
+    use ManagesLineItems;
12
     use RedirectToListPage;
16
     use RedirectToListPage;
13
 
17
 
14
     protected static string $resource = BillResource::class;
18
     protected static string $resource = BillResource::class;
17
     {
21
     {
18
         return MaxWidth::Full;
22
         return MaxWidth::Full;
19
     }
23
     }
24
+
25
+    protected function handleRecordCreation(array $data): Model
26
+    {
27
+        /** @var Bill $record */
28
+        $record = parent::handleRecordCreation($data);
29
+
30
+        $this->handleLineItems($record, collect($data['lineItems'] ?? []));
31
+
32
+        $totals = $this->updateDocumentTotals($record, $data);
33
+
34
+        $record->updateQuietly($totals);
35
+
36
+        if (! $record->initialTransaction) {
37
+            $record->createInitialTransaction();
38
+        }
39
+
40
+        return $record;
41
+    }
20
 }
42
 }

+ 28
- 0
app/Filament/Company/Resources/Purchases/BillResource/Pages/EditBill.php 查看文件

2
 
2
 
3
 namespace App\Filament\Company\Resources\Purchases\BillResource\Pages;
3
 namespace App\Filament\Company\Resources\Purchases\BillResource\Pages;
4
 
4
 
5
+use App\Concerns\ManagesLineItems;
5
 use App\Concerns\RedirectToListPage;
6
 use App\Concerns\RedirectToListPage;
6
 use App\Filament\Company\Resources\Purchases\BillResource;
7
 use App\Filament\Company\Resources\Purchases\BillResource;
8
+use App\Models\Accounting\Bill;
7
 use Filament\Actions;
9
 use Filament\Actions;
8
 use Filament\Resources\Pages\EditRecord;
10
 use Filament\Resources\Pages\EditRecord;
9
 use Filament\Support\Enums\MaxWidth;
11
 use Filament\Support\Enums\MaxWidth;
12
+use Illuminate\Database\Eloquent\Model;
10
 
13
 
11
 class EditBill extends EditRecord
14
 class EditBill extends EditRecord
12
 {
15
 {
16
+    use ManagesLineItems;
13
     use RedirectToListPage;
17
     use RedirectToListPage;
14
 
18
 
15
     protected static string $resource = BillResource::class;
19
     protected static string $resource = BillResource::class;
25
     {
29
     {
26
         return MaxWidth::Full;
30
         return MaxWidth::Full;
27
     }
31
     }
32
+
33
+    protected function handleRecordUpdate(Model $record, array $data): Model
34
+    {
35
+        /** @var Bill $record */
36
+        $lineItems = collect($data['lineItems'] ?? []);
37
+
38
+        $this->deleteRemovedLineItems($record, $lineItems);
39
+
40
+        $this->handleLineItems($record, $lineItems);
41
+
42
+        $totals = $this->updateDocumentTotals($record, $data);
43
+
44
+        $data = array_merge($data, $totals);
45
+
46
+        $record = parent::handleRecordUpdate($record, $data);
47
+
48
+        if (! $record->initialTransaction) {
49
+            $record->createInitialTransaction();
50
+        } else {
51
+            $record->updateInitialTransaction();
52
+        }
53
+
54
+        return $record;
55
+    }
28
 }
56
 }

+ 52
- 121
app/Filament/Company/Resources/Sales/InvoiceResource.php 查看文件

3
 namespace App\Filament\Company\Resources\Sales;
3
 namespace App\Filament\Company\Resources\Sales;
4
 
4
 
5
 use App\Collections\Accounting\InvoiceCollection;
5
 use App\Collections\Accounting\InvoiceCollection;
6
+use App\Enums\Accounting\DocumentDiscountMethod;
6
 use App\Enums\Accounting\InvoiceStatus;
7
 use App\Enums\Accounting\InvoiceStatus;
7
 use App\Enums\Accounting\PaymentMethod;
8
 use App\Enums\Accounting\PaymentMethod;
8
 use App\Filament\Company\Resources\Sales\InvoiceResource\Pages;
9
 use App\Filament\Company\Resources\Sales\InvoiceResource\Pages;
9
 use App\Filament\Company\Resources\Sales\InvoiceResource\RelationManagers;
10
 use App\Filament\Company\Resources\Sales\InvoiceResource\RelationManagers;
10
 use App\Filament\Company\Resources\Sales\InvoiceResource\Widgets;
11
 use App\Filament\Company\Resources\Sales\InvoiceResource\Widgets;
12
+use App\Filament\Forms\Components\InvoiceTotals;
11
 use App\Filament\Tables\Actions\ReplicateBulkAction;
13
 use App\Filament\Tables\Actions\ReplicateBulkAction;
12
 use App\Filament\Tables\Filters\DateRangeFilter;
14
 use App\Filament\Tables\Filters\DateRangeFilter;
13
 use App\Models\Accounting\Adjustment;
15
 use App\Models\Accounting\Adjustment;
14
-use App\Models\Accounting\DocumentLineItem;
15
 use App\Models\Accounting\Invoice;
16
 use App\Models\Accounting\Invoice;
16
 use App\Models\Banking\BankAccount;
17
 use App\Models\Banking\BankAccount;
17
 use App\Models\Common\Offering;
18
 use App\Models\Common\Offering;
30
 use Filament\Tables\Table;
31
 use Filament\Tables\Table;
31
 use Illuminate\Database\Eloquent\Builder;
32
 use Illuminate\Database\Eloquent\Builder;
32
 use Illuminate\Database\Eloquent\Collection;
33
 use Illuminate\Database\Eloquent\Collection;
33
-use Illuminate\Database\Eloquent\Model;
34
 use Illuminate\Support\Facades\Auth;
34
 use Illuminate\Support\Facades\Auth;
35
 use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
35
 use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
36
 
36
 
128
                                     ->minDate(static function (Forms\Get $get) {
128
                                     ->minDate(static function (Forms\Get $get) {
129
                                         return $get('date') ?? now();
129
                                         return $get('date') ?? now();
130
                                     }),
130
                                     }),
131
+                                Forms\Components\Select::make('discount_method')
132
+                                    ->label('Discount Method')
133
+                                    ->options(DocumentDiscountMethod::class)
134
+                                    ->selectablePlaceholder(false)
135
+                                    ->default(DocumentDiscountMethod::PerLineItem)
136
+                                    ->afterStateUpdated(function ($state, Forms\Set $set) {
137
+                                        $discountMethod = DocumentDiscountMethod::parse($state);
138
+
139
+                                        if ($discountMethod->isPerDocument()) {
140
+                                            $set('lineItems.*.salesDiscounts', []);
141
+                                        }
142
+                                    })
143
+                                    ->live(),
131
                             ])->grow(true),
144
                             ])->grow(true),
132
                         ])->from('md'),
145
                         ])->from('md'),
133
                         TableRepeater::make('lineItems')
146
                         TableRepeater::make('lineItems')
134
                             ->relationship()
147
                             ->relationship()
135
-                            ->saveRelationshipsUsing(function (TableRepeater $component, Forms\Contracts\HasForms $livewire, ?array $state) {
136
-                                if (! is_array($state)) {
137
-                                    $state = [];
138
-                                }
139
-
140
-                                $relationship = $component->getRelationship();
141
-
142
-                                $existingRecords = $component->getCachedExistingRecords();
143
-
144
-                                $recordsToDelete = [];
145
-
146
-                                foreach ($existingRecords->pluck($relationship->getRelated()->getKeyName()) as $keyToCheckForDeletion) {
147
-                                    if (array_key_exists("record-{$keyToCheckForDeletion}", $state)) {
148
-                                        continue;
149
-                                    }
150
-
151
-                                    $recordsToDelete[] = $keyToCheckForDeletion;
152
-                                    $existingRecords->forget("record-{$keyToCheckForDeletion}");
153
-                                }
154
-
155
-                                $relationship
156
-                                    ->whereKey($recordsToDelete)
157
-                                    ->get()
158
-                                    ->each(static fn (Model $record) => $record->delete());
159
-
160
-                                $childComponentContainers = $component->getChildComponentContainers(
161
-                                    withHidden: $component->shouldSaveRelationshipsWhenHidden(),
162
-                                );
163
-
164
-                                $itemOrder = 1;
165
-                                $orderColumn = $component->getOrderColumn();
166
-
167
-                                $translatableContentDriver = $livewire->makeFilamentTranslatableContentDriver();
168
-
169
-                                foreach ($childComponentContainers as $itemKey => $item) {
170
-                                    $itemData = $item->getState(shouldCallHooksBefore: false);
171
-
172
-                                    if ($orderColumn) {
173
-                                        $itemData[$orderColumn] = $itemOrder;
174
-
175
-                                        $itemOrder++;
176
-                                    }
177
-
178
-                                    if ($record = ($existingRecords[$itemKey] ?? null)) {
179
-                                        $itemData = $component->mutateRelationshipDataBeforeSave($itemData, record: $record);
180
-
181
-                                        if ($itemData === null) {
182
-                                            continue;
183
-                                        }
184
-
185
-                                        $translatableContentDriver ?
186
-                                            $translatableContentDriver->updateRecord($record, $itemData) :
187
-                                            $record->fill($itemData)->save();
188
-
189
-                                        continue;
190
-                                    }
191
-
192
-                                    $relatedModel = $component->getRelatedModel();
193
-
194
-                                    $itemData = $component->mutateRelationshipDataBeforeCreate($itemData);
195
-
196
-                                    if ($itemData === null) {
197
-                                        continue;
198
-                                    }
199
-
200
-                                    if ($translatableContentDriver) {
201
-                                        $record = $translatableContentDriver->makeRecord($relatedModel, $itemData);
202
-                                    } else {
203
-                                        $record = new $relatedModel;
204
-                                        $record->fill($itemData);
205
-                                    }
206
-
207
-                                    $record = $relationship->save($record);
208
-                                    $item->model($record)->saveRelationships();
209
-                                    $existingRecords->push($record);
148
+                            ->saveRelationshipsUsing(null)
149
+                            ->dehydrated(true)
150
+                            ->headers(function (Forms\Get $get) {
151
+                                $hasDiscounts = DocumentDiscountMethod::parse($get('discount_method'))->isPerLineItem();
152
+
153
+                                $headers = [
154
+                                    Header::make('Items')->width($hasDiscounts ? '15%' : '20%'),
155
+                                    Header::make('Description')->width($hasDiscounts ? '25%' : '30%'),  // Increase when no discounts
156
+                                    Header::make('Quantity')->width('10%'),
157
+                                    Header::make('Price')->width('10%'),
158
+                                    Header::make('Taxes')->width($hasDiscounts ? '15%' : '20%'),       // Increase when no discounts
159
+                                ];
160
+
161
+                                if ($hasDiscounts) {
162
+                                    $headers[] = Header::make('Discounts')->width('15%');
210
                                 }
163
                                 }
211
 
164
 
212
-                                $component->getRecord()->setRelation($component->getRelationshipName(), $existingRecords);
213
-
214
-                                /** @var Invoice $invoice */
215
-                                $invoice = $component->getRecord();
216
-
217
-                                // Recalculate totals for line items
218
-                                $invoice->lineItems()->each(function (DocumentLineItem $lineItem) {
219
-                                    $lineItem->updateQuietly([
220
-                                        'tax_total' => $lineItem->calculateTaxTotal()->getAmount(),
221
-                                        'discount_total' => $lineItem->calculateDiscountTotal()->getAmount(),
222
-                                    ]);
223
-                                });
224
-
225
-                                $subtotal = $invoice->lineItems()->sum('subtotal') / 100;
226
-                                $taxTotal = $invoice->lineItems()->sum('tax_total') / 100;
227
-                                $discountTotal = $invoice->lineItems()->sum('discount_total') / 100;
228
-                                $grandTotal = $subtotal + $taxTotal - $discountTotal;
229
-
230
-                                $invoice->updateQuietly([
231
-                                    'subtotal' => $subtotal,
232
-                                    'tax_total' => $taxTotal,
233
-                                    'discount_total' => $discountTotal,
234
-                                    'total' => $grandTotal,
235
-                                ]);
165
+                                $headers[] = Header::make('Amount')->width('10%')->align('right');
236
 
166
 
237
-                                if ($invoice->approved_at && $invoice->approvalTransaction) {
238
-                                    $invoice->updateApprovalTransaction();
239
-                                }
167
+                                return $headers;
240
                             })
168
                             })
241
-                            ->headers([
242
-                                Header::make('Items')->width('15%'),
243
-                                Header::make('Description')->width('25%'),
244
-                                Header::make('Quantity')->width('10%'),
245
-                                Header::make('Price')->width('10%'),
246
-                                Header::make('Taxes')->width('15%'),
247
-                                Header::make('Discounts')->width('15%'),
248
-                                Header::make('Amount')->width('10%')->align('right'),
249
-                            ])
250
                             ->schema([
169
                             ->schema([
251
                                 Forms\Components\Select::make('offering_id')
170
                                 Forms\Components\Select::make('offering_id')
252
                                     ->relationship('sellableOffering', 'name')
171
                                     ->relationship('sellableOffering', 'name')
262
                                             $set('description', $offeringRecord->description);
181
                                             $set('description', $offeringRecord->description);
263
                                             $set('unit_price', $offeringRecord->price);
182
                                             $set('unit_price', $offeringRecord->price);
264
                                             $set('salesTaxes', $offeringRecord->salesTaxes->pluck('id')->toArray());
183
                                             $set('salesTaxes', $offeringRecord->salesTaxes->pluck('id')->toArray());
265
-                                            $set('salesDiscounts', $offeringRecord->salesDiscounts->pluck('id')->toArray());
184
+
185
+                                            $discountMethod = DocumentDiscountMethod::parse($get('../../discount_method'));
186
+                                            if ($discountMethod->isPerLineItem()) {
187
+                                                $set('salesDiscounts', $offeringRecord->salesDiscounts->pluck('id')->toArray());
188
+                                            }
266
                                         }
189
                                         }
267
                                     }),
190
                                     }),
268
                                 Forms\Components\TextInput::make('description'),
191
                                 Forms\Components\TextInput::make('description'),
278
                                     ->default(0),
201
                                     ->default(0),
279
                                 Forms\Components\Select::make('salesTaxes')
202
                                 Forms\Components\Select::make('salesTaxes')
280
                                     ->relationship('salesTaxes', 'name')
203
                                     ->relationship('salesTaxes', 'name')
204
+                                    ->saveRelationshipsUsing(null)
205
+                                    ->dehydrated(true)
281
                                     ->preload()
206
                                     ->preload()
282
                                     ->multiple()
207
                                     ->multiple()
283
                                     ->live()
208
                                     ->live()
284
                                     ->searchable(),
209
                                     ->searchable(),
285
                                 Forms\Components\Select::make('salesDiscounts')
210
                                 Forms\Components\Select::make('salesDiscounts')
286
                                     ->relationship('salesDiscounts', 'name')
211
                                     ->relationship('salesDiscounts', 'name')
212
+                                    ->saveRelationshipsUsing(null)
213
+                                    ->dehydrated(true)
287
                                     ->preload()
214
                                     ->preload()
288
                                     ->multiple()
215
                                     ->multiple()
289
                                     ->live()
216
                                     ->live()
217
+                                    ->hidden(function (Forms\Get $get) {
218
+                                        $discountMethod = DocumentDiscountMethod::parse($get('../../discount_method'));
219
+
220
+                                        return $discountMethod->isPerDocument();
221
+                                    })
290
                                     ->searchable(),
222
                                     ->searchable(),
291
                                 Forms\Components\Placeholder::make('total')
223
                                 Forms\Components\Placeholder::make('total')
292
                                     ->hiddenLabel()
224
                                     ->hiddenLabel()
318
                                         return CurrencyConverter::formatToMoney($total);
250
                                         return CurrencyConverter::formatToMoney($total);
319
                                     }),
251
                                     }),
320
                             ]),
252
                             ]),
321
-                        Forms\Components\Grid::make(6)
322
-                            ->schema([
323
-                                Forms\Components\ViewField::make('totals')
324
-                                    ->columnStart(5)
325
-                                    ->columnSpan(2)
326
-                                    ->view('filament.forms.components.invoice-totals'),
327
-                            ]),
253
+                        InvoiceTotals::make(),
328
                         Forms\Components\Textarea::make('terms')
254
                         Forms\Components\Textarea::make('terms')
329
                             ->columnSpanFull(),
255
                             ->columnSpanFull(),
330
                     ]),
256
                     ]),
342
         return $table
268
         return $table
343
             ->defaultSort('due_date')
269
             ->defaultSort('due_date')
344
             ->columns([
270
             ->columns([
271
+                Tables\Columns\TextColumn::make('id')
272
+                    ->label('ID')
273
+                    ->sortable()
274
+                    ->toggleable(isToggledHiddenByDefault: true)
275
+                    ->searchable(),
345
                 Tables\Columns\TextColumn::make('status')
276
                 Tables\Columns\TextColumn::make('status')
346
                     ->badge()
277
                     ->badge()
347
                     ->searchable(),
278
                     ->searchable(),

+ 18
- 0
app/Filament/Company/Resources/Sales/InvoiceResource/Pages/CreateInvoice.php 查看文件

2
 
2
 
3
 namespace App\Filament\Company\Resources\Sales\InvoiceResource\Pages;
3
 namespace App\Filament\Company\Resources\Sales\InvoiceResource\Pages;
4
 
4
 
5
+use App\Concerns\ManagesLineItems;
5
 use App\Concerns\RedirectToListPage;
6
 use App\Concerns\RedirectToListPage;
6
 use App\Filament\Company\Resources\Sales\InvoiceResource;
7
 use App\Filament\Company\Resources\Sales\InvoiceResource;
8
+use App\Models\Accounting\Invoice;
7
 use Filament\Resources\Pages\CreateRecord;
9
 use Filament\Resources\Pages\CreateRecord;
8
 use Filament\Support\Enums\MaxWidth;
10
 use Filament\Support\Enums\MaxWidth;
11
+use Illuminate\Database\Eloquent\Model;
9
 
12
 
10
 class CreateInvoice extends CreateRecord
13
 class CreateInvoice extends CreateRecord
11
 {
14
 {
15
+    use ManagesLineItems;
12
     use RedirectToListPage;
16
     use RedirectToListPage;
13
 
17
 
14
     protected static string $resource = InvoiceResource::class;
18
     protected static string $resource = InvoiceResource::class;
17
     {
21
     {
18
         return MaxWidth::Full;
22
         return MaxWidth::Full;
19
     }
23
     }
24
+
25
+    protected function handleRecordCreation(array $data): Model
26
+    {
27
+        /** @var Invoice $record */
28
+        $record = parent::handleRecordCreation($data);
29
+
30
+        $this->handleLineItems($record, collect($data['lineItems'] ?? []));
31
+
32
+        $totals = $this->updateDocumentTotals($record, $data);
33
+
34
+        $record->updateQuietly($totals);
35
+
36
+        return $record;
37
+    }
20
 }
38
 }

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

2
 
2
 
3
 namespace App\Filament\Company\Resources\Sales\InvoiceResource\Pages;
3
 namespace App\Filament\Company\Resources\Sales\InvoiceResource\Pages;
4
 
4
 
5
+use App\Concerns\ManagesLineItems;
5
 use App\Concerns\RedirectToListPage;
6
 use App\Concerns\RedirectToListPage;
6
 use App\Filament\Company\Resources\Sales\InvoiceResource;
7
 use App\Filament\Company\Resources\Sales\InvoiceResource;
8
+use App\Models\Accounting\Invoice;
7
 use Filament\Actions;
9
 use Filament\Actions;
8
 use Filament\Resources\Pages\EditRecord;
10
 use Filament\Resources\Pages\EditRecord;
9
 use Filament\Support\Enums\MaxWidth;
11
 use Filament\Support\Enums\MaxWidth;
12
+use Illuminate\Database\Eloquent\Model;
10
 
13
 
11
 class EditInvoice extends EditRecord
14
 class EditInvoice extends EditRecord
12
 {
15
 {
16
+    use ManagesLineItems;
13
     use RedirectToListPage;
17
     use RedirectToListPage;
14
 
18
 
15
     protected static string $resource = InvoiceResource::class;
19
     protected static string $resource = InvoiceResource::class;
25
     {
29
     {
26
         return MaxWidth::Full;
30
         return MaxWidth::Full;
27
     }
31
     }
32
+
33
+    protected function handleRecordUpdate(Model $record, array $data): Model
34
+    {
35
+        /** @var Invoice $record */
36
+        $lineItems = collect($data['lineItems'] ?? []);
37
+
38
+        $this->deleteRemovedLineItems($record, $lineItems);
39
+
40
+        $this->handleLineItems($record, $lineItems);
41
+
42
+        $totals = $this->updateDocumentTotals($record, $data);
43
+
44
+        $data = array_merge($data, $totals);
45
+
46
+        $record = parent::handleRecordUpdate($record, $data);
47
+
48
+        if ($record->approved_at && $record->approvalTransaction) {
49
+            $record->updateApprovalTransaction();
50
+        }
51
+
52
+        return $record;
53
+    }
28
 }
54
 }

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

6
 use App\Filament\Company\Resources\Sales\InvoiceResource;
6
 use App\Filament\Company\Resources\Sales\InvoiceResource;
7
 use App\Models\Accounting\Invoice;
7
 use App\Models\Accounting\Invoice;
8
 use Filament\Actions;
8
 use Filament\Actions;
9
+use Filament\Infolists\Components\Grid;
9
 use Filament\Infolists\Components\Section;
10
 use Filament\Infolists\Components\Section;
10
 use Filament\Infolists\Components\TextEntry;
11
 use Filament\Infolists\Components\TextEntry;
12
+use Filament\Infolists\Components\ViewEntry;
11
 use Filament\Infolists\Infolist;
13
 use Filament\Infolists\Infolist;
12
 use Filament\Resources\Pages\ViewRecord;
14
 use Filament\Resources\Pages\ViewRecord;
13
 use Filament\Support\Enums\FontWeight;
15
 use Filament\Support\Enums\FontWeight;
14
 use Filament\Support\Enums\IconPosition;
16
 use Filament\Support\Enums\IconPosition;
15
 use Filament\Support\Enums\IconSize;
17
 use Filament\Support\Enums\IconSize;
18
+use Filament\Support\Enums\MaxWidth;
16
 
19
 
17
 class ViewInvoice extends ViewRecord
20
 class ViewInvoice extends ViewRecord
18
 {
21
 {
22
+    protected static string $view = 'filament.company.resources.sales.invoices.pages.view-invoice';
23
+
19
     protected static string $resource = InvoiceResource::class;
24
     protected static string $resource = InvoiceResource::class;
20
 
25
 
21
     protected $listeners = [
26
     protected $listeners = [
22
         'refresh' => '$refresh',
27
         'refresh' => '$refresh',
23
     ];
28
     ];
24
 
29
 
30
+    public function getMaxContentWidth(): MaxWidth | string | null
31
+    {
32
+        return MaxWidth::SixExtraLarge;
33
+    }
34
+
25
     protected function getHeaderActions(): array
35
     protected function getHeaderActions(): array
26
     {
36
     {
27
         return [
37
         return [
49
                 Section::make('Invoice Details')
59
                 Section::make('Invoice Details')
50
                     ->columns(4)
60
                     ->columns(4)
51
                     ->schema([
61
                     ->schema([
52
-                        TextEntry::make('invoice_number')
53
-                            ->label('Invoice #'),
54
-                        TextEntry::make('status')
55
-                            ->badge(),
56
-                        TextEntry::make('client.name')
57
-                            ->label('Client')
58
-                            ->color('primary')
59
-                            ->weight(FontWeight::SemiBold)
60
-                            ->url(static fn (Invoice $record) => ClientResource::getUrl('edit', ['record' => $record->client_id])),
61
-                        TextEntry::make('total')
62
-                            ->label('Total')
63
-                            ->money(),
64
-                        TextEntry::make('amount_due')
65
-                            ->label('Amount Due')
66
-                            ->money(),
67
-                        TextEntry::make('date')
68
-                            ->label('Date')
69
-                            ->date(),
70
-                        TextEntry::make('due_date')
71
-                            ->label('Due')
72
-                            ->asRelativeDay(),
73
-                        TextEntry::make('approved_at')
74
-                            ->label('Approved At')
75
-                            ->placeholder('Not Approved')
76
-                            ->date(),
77
-                        TextEntry::make('last_sent')
78
-                            ->label('Last Sent')
79
-                            ->placeholder('Never')
80
-                            ->date(),
81
-                        TextEntry::make('paid_at')
82
-                            ->label('Paid At')
83
-                            ->placeholder('Not Paid')
84
-                            ->date(),
62
+                        Grid::make(1)
63
+                            ->schema([
64
+                                TextEntry::make('invoice_number')
65
+                                    ->label('Invoice #'),
66
+                                TextEntry::make('status')
67
+                                    ->badge(),
68
+                                TextEntry::make('client.name')
69
+                                    ->label('Client')
70
+                                    ->color('primary')
71
+                                    ->weight(FontWeight::SemiBold)
72
+                                    ->url(static fn (Invoice $record) => ClientResource::getUrl('edit', ['record' => $record->client_id])),
73
+                                TextEntry::make('amount_due')
74
+                                    ->label('Amount Due')
75
+                                    ->money(),
76
+                                TextEntry::make('due_date')
77
+                                    ->label('Due')
78
+                                    ->asRelativeDay(),
79
+                                TextEntry::make('approved_at')
80
+                                    ->label('Approved At')
81
+                                    ->placeholder('Not Approved')
82
+                                    ->date(),
83
+                                TextEntry::make('last_sent')
84
+                                    ->label('Last Sent')
85
+                                    ->placeholder('Never')
86
+                                    ->date(),
87
+                                TextEntry::make('paid_at')
88
+                                    ->label('Paid At')
89
+                                    ->placeholder('Not Paid')
90
+                                    ->date(),
91
+                            ])->columnSpan(1),
92
+                        Grid::make()
93
+                            ->schema([
94
+                                ViewEntry::make('invoice-view')
95
+                                    ->label('View Invoice')
96
+                                    ->columnSpan(3)
97
+                                    ->view('filament.company.resources.sales.invoices.components.invoice-view')
98
+                                    ->viewData([
99
+                                        'invoice' => $this->record,
100
+                                    ]),
101
+                            ])
102
+                            ->columnSpan(3),
85
                     ]),
103
                     ]),
86
             ]);
104
             ]);
87
     }
105
     }

+ 2
- 0
app/Filament/Company/Resources/Sales/InvoiceResource/RelationManagers/PaymentsRelationManager.php 查看文件

28
 
28
 
29
     protected static ?string $modelLabel = 'Payment';
29
     protected static ?string $modelLabel = 'Payment';
30
 
30
 
31
+    protected static bool $isLazy = false;
32
+
31
     protected $listeners = [
33
     protected $listeners = [
32
         'refresh' => '$refresh',
34
         'refresh' => '$refresh',
33
     ];
35
     ];

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

45
 
45
 
46
         $totalValidInvoiceCount = $validInvoices->count();
46
         $totalValidInvoiceCount = $validInvoices->count();
47
 
47
 
48
-        $averageInvoiceTotal = $totalValidInvoiceCount > 0 ? $totalValidInvoiceAmount / $totalValidInvoiceCount : 0;
48
+        $averageInvoiceTotal = $totalValidInvoiceCount > 0
49
+            ? (int) round($totalValidInvoiceAmount / $totalValidInvoiceCount)
50
+            : 0;
49
 
51
 
50
         $averagePaymentTime = $this->getPageTableQuery()
52
         $averagePaymentTime = $this->getPageTableQuery()
51
             ->whereNotNull('paid_at')
53
             ->whereNotNull('paid_at')

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

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

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

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

+ 10
- 0
app/Models/Accounting/Account.php 查看文件

133
         return self::where('name', 'Accounts Payable')->firstOrFail();
133
         return self::where('name', 'Accounts Payable')->firstOrFail();
134
     }
134
     }
135
 
135
 
136
+    public static function getSalesDiscountAccount(): self
137
+    {
138
+        return self::where('name', 'Sales Discount')->firstOrFail();
139
+    }
140
+
141
+    public static function getPurchaseDiscountAccount(): self
142
+    {
143
+        return self::where('name', 'Purchase Discount')->firstOrFail();
144
+    }
145
+
136
     protected static function newFactory(): Factory
146
     protected static function newFactory(): Factory
137
     {
147
     {
138
         return AccountFactory::new();
148
         return AccountFactory::new();

+ 38
- 1
app/Models/Accounting/Bill.php 查看文件

3
 namespace App\Models\Accounting;
3
 namespace App\Models\Accounting;
4
 
4
 
5
 use App\Casts\MoneyCast;
5
 use App\Casts\MoneyCast;
6
+use App\Casts\RateCast;
6
 use App\Concerns\Blamable;
7
 use App\Concerns\Blamable;
7
 use App\Concerns\CompanyOwned;
8
 use App\Concerns\CompanyOwned;
9
+use App\Enums\Accounting\AdjustmentComputation;
8
 use App\Enums\Accounting\BillStatus;
10
 use App\Enums\Accounting\BillStatus;
11
+use App\Enums\Accounting\DocumentDiscountMethod;
9
 use App\Enums\Accounting\JournalEntryType;
12
 use App\Enums\Accounting\JournalEntryType;
10
 use App\Enums\Accounting\TransactionType;
13
 use App\Enums\Accounting\TransactionType;
11
 use App\Filament\Company\Resources\Purchases\BillResource;
14
 use App\Filament\Company\Resources\Purchases\BillResource;
12
 use App\Models\Common\Vendor;
15
 use App\Models\Common\Vendor;
13
 use App\Observers\BillObserver;
16
 use App\Observers\BillObserver;
17
+use App\Utilities\Currency\CurrencyConverter;
14
 use Filament\Actions\MountableAction;
18
 use Filament\Actions\MountableAction;
15
 use Filament\Actions\ReplicateAction;
19
 use Filament\Actions\ReplicateAction;
16
 use Illuminate\Database\Eloquent\Attributes\ObservedBy;
20
 use Illuminate\Database\Eloquent\Attributes\ObservedBy;
42
         'paid_at',
46
         'paid_at',
43
         'status',
47
         'status',
44
         'currency_code',
48
         'currency_code',
49
+        'discount_method',
50
+        'discount_computation',
51
+        'discount_rate',
45
         'subtotal',
52
         'subtotal',
46
         'tax_total',
53
         'tax_total',
47
         'discount_total',
54
         'discount_total',
57
         'due_date' => 'date',
64
         'due_date' => 'date',
58
         'paid_at' => 'datetime',
65
         'paid_at' => 'datetime',
59
         'status' => BillStatus::class,
66
         'status' => BillStatus::class,
67
+        'discount_method' => DocumentDiscountMethod::class,
68
+        'discount_computation' => AdjustmentComputation::class,
69
+        'discount_rate' => RateCast::class,
60
         'subtotal' => MoneyCast::class,
70
         'subtotal' => MoneyCast::class,
61
         'tax_total' => MoneyCast::class,
71
         'tax_total' => MoneyCast::class,
62
         'discount_total' => MoneyCast::class,
72
         'discount_total' => MoneyCast::class,
215
             'description' => $baseDescription,
225
             'description' => $baseDescription,
216
         ]);
226
         ]);
217
 
227
 
218
-        foreach ($this->lineItems as $lineItem) {
228
+        $totalLineItemSubtotal = (int) $this->lineItems()->sum('subtotal');
229
+        $billDiscountTotalCents = (int) $this->getRawOriginal('discount_total');
230
+        $remainingDiscountCents = $billDiscountTotalCents;
231
+
232
+        foreach ($this->lineItems as $index => $lineItem) {
219
             $lineItemDescription = "{$baseDescription} › {$lineItem->offering->name}";
233
             $lineItemDescription = "{$baseDescription} › {$lineItem->offering->name}";
220
 
234
 
221
             $transaction->journalEntries()->create([
235
             $transaction->journalEntries()->create([
245
                     ]);
259
                     ]);
246
                 }
260
                 }
247
             }
261
             }
262
+
263
+            if ($this->discount_method->isPerDocument() && $totalLineItemSubtotal > 0) {
264
+                $lineItemSubtotalCents = (int) $lineItem->getRawOriginal('subtotal');
265
+
266
+                if ($index === $this->lineItems->count() - 1) {
267
+                    $lineItemDiscount = $remainingDiscountCents;
268
+                } else {
269
+                    $lineItemDiscount = (int) round(
270
+                        ($lineItemSubtotalCents / $totalLineItemSubtotal) * $billDiscountTotalCents
271
+                    );
272
+                    $remainingDiscountCents -= $lineItemDiscount;
273
+                }
274
+
275
+                if ($lineItemDiscount > 0) {
276
+                    $transaction->journalEntries()->create([
277
+                        'company_id' => $this->company_id,
278
+                        'type' => JournalEntryType::Credit,
279
+                        'account_id' => Account::getPurchaseDiscountAccount()->id,
280
+                        'amount' => CurrencyConverter::convertCentsToFormatSimple($lineItemDiscount),
281
+                        'description' => "{$lineItemDescription} (Proportional Discount)",
282
+                    ]);
283
+                }
284
+            }
248
         }
285
         }
249
     }
286
     }
250
 
287
 

+ 38
- 1
app/Models/Accounting/Invoice.php 查看文件

3
 namespace App\Models\Accounting;
3
 namespace App\Models\Accounting;
4
 
4
 
5
 use App\Casts\MoneyCast;
5
 use App\Casts\MoneyCast;
6
+use App\Casts\RateCast;
6
 use App\Collections\Accounting\InvoiceCollection;
7
 use App\Collections\Accounting\InvoiceCollection;
7
 use App\Concerns\Blamable;
8
 use App\Concerns\Blamable;
8
 use App\Concerns\CompanyOwned;
9
 use App\Concerns\CompanyOwned;
10
+use App\Enums\Accounting\AdjustmentComputation;
11
+use App\Enums\Accounting\DocumentDiscountMethod;
9
 use App\Enums\Accounting\InvoiceStatus;
12
 use App\Enums\Accounting\InvoiceStatus;
10
 use App\Enums\Accounting\JournalEntryType;
13
 use App\Enums\Accounting\JournalEntryType;
11
 use App\Enums\Accounting\TransactionType;
14
 use App\Enums\Accounting\TransactionType;
12
 use App\Filament\Company\Resources\Sales\InvoiceResource;
15
 use App\Filament\Company\Resources\Sales\InvoiceResource;
13
 use App\Models\Common\Client;
16
 use App\Models\Common\Client;
14
 use App\Observers\InvoiceObserver;
17
 use App\Observers\InvoiceObserver;
18
+use App\Utilities\Currency\CurrencyConverter;
15
 use Filament\Actions\Action;
19
 use Filament\Actions\Action;
16
 use Filament\Actions\MountableAction;
20
 use Filament\Actions\MountableAction;
17
 use Filament\Actions\ReplicateAction;
21
 use Filament\Actions\ReplicateAction;
51
         'last_sent',
55
         'last_sent',
52
         'status',
56
         'status',
53
         'currency_code',
57
         'currency_code',
58
+        'discount_method',
59
+        'discount_computation',
60
+        'discount_rate',
54
         'subtotal',
61
         'subtotal',
55
         'tax_total',
62
         'tax_total',
56
         'discount_total',
63
         'discount_total',
69
         'paid_at' => 'datetime',
76
         'paid_at' => 'datetime',
70
         'last_sent' => 'datetime',
77
         'last_sent' => 'datetime',
71
         'status' => InvoiceStatus::class,
78
         'status' => InvoiceStatus::class,
79
+        'discount_method' => DocumentDiscountMethod::class,
80
+        'discount_computation' => AdjustmentComputation::class,
81
+        'discount_rate' => RateCast::class,
72
         'subtotal' => MoneyCast::class,
82
         'subtotal' => MoneyCast::class,
73
         'tax_total' => MoneyCast::class,
83
         'tax_total' => MoneyCast::class,
74
         'discount_total' => MoneyCast::class,
84
         'discount_total' => MoneyCast::class,
260
             'description' => $baseDescription,
270
             'description' => $baseDescription,
261
         ]);
271
         ]);
262
 
272
 
263
-        foreach ($this->lineItems as $lineItem) {
273
+        $totalLineItemSubtotal = (int) $this->lineItems()->sum('subtotal');
274
+        $invoiceDiscountTotalCents = (int) $this->getRawOriginal('discount_total');
275
+        $remainingDiscountCents = $invoiceDiscountTotalCents;
276
+
277
+        foreach ($this->lineItems as $index => $lineItem) {
264
             $lineItemDescription = "{$baseDescription} › {$lineItem->offering->name}";
278
             $lineItemDescription = "{$baseDescription} › {$lineItem->offering->name}";
265
 
279
 
266
             $transaction->journalEntries()->create([
280
             $transaction->journalEntries()->create([
280
                     'description' => $lineItemDescription,
294
                     'description' => $lineItemDescription,
281
                 ]);
295
                 ]);
282
             }
296
             }
297
+
298
+            if ($this->discount_method->isPerDocument() && $totalLineItemSubtotal > 0) {
299
+                $lineItemSubtotalCents = (int) $lineItem->getRawOriginal('subtotal');
300
+
301
+                if ($index === $this->lineItems->count() - 1) {
302
+                    $lineItemDiscount = $remainingDiscountCents;
303
+                } else {
304
+                    $lineItemDiscount = (int) round(
305
+                        ($lineItemSubtotalCents / $totalLineItemSubtotal) * $invoiceDiscountTotalCents
306
+                    );
307
+                    $remainingDiscountCents -= $lineItemDiscount;
308
+                }
309
+
310
+                if ($lineItemDiscount > 0) {
311
+                    $transaction->journalEntries()->create([
312
+                        'company_id' => $this->company_id,
313
+                        'type' => JournalEntryType::Debit,
314
+                        'account_id' => Account::getSalesDiscountAccount()->id,
315
+                        'amount' => CurrencyConverter::convertCentsToFormatSimple($lineItemDiscount),
316
+                        'description' => "{$lineItemDescription} (Proportional Discount)",
317
+                    ]);
318
+                }
319
+            }
283
         }
320
         }
284
     }
321
     }
285
 
322
 

+ 0
- 82
app/Models/Setting/Discount.php 查看文件

1
-<?php
2
-
3
-namespace App\Models\Setting;
4
-
5
-use App\Casts\RateCast;
6
-use App\Concerns\Blamable;
7
-use App\Concerns\CompanyOwned;
8
-use App\Concerns\HasDefault;
9
-use App\Concerns\SyncsWithCompanyDefaults;
10
-use App\Enums\Setting\DiscountComputation;
11
-use App\Enums\Setting\DiscountScope;
12
-use App\Enums\Setting\DiscountType;
13
-use App\Models\Accounting\Account;
14
-use Database\Factories\Setting\DiscountFactory;
15
-use Illuminate\Database\Eloquent\Factories\Factory;
16
-use Illuminate\Database\Eloquent\Factories\HasFactory;
17
-use Illuminate\Database\Eloquent\Model;
18
-use Illuminate\Database\Eloquent\Relations\BelongsTo;
19
-use Illuminate\Database\Eloquent\Relations\HasOne;
20
-use Illuminate\Database\Eloquent\Relations\MorphTo;
21
-
22
-class Discount extends Model
23
-{
24
-    use Blamable;
25
-    use CompanyOwned;
26
-    use HasDefault;
27
-    use HasFactory;
28
-    use SyncsWithCompanyDefaults;
29
-
30
-    protected $table = 'discounts';
31
-
32
-    protected $fillable = [
33
-        'company_id',
34
-        'account_id',
35
-        'rate',
36
-        'computation',
37
-        'type',
38
-        'scope',
39
-        'start_date',
40
-        'end_date',
41
-        'enabled',
42
-        'created_by',
43
-        'updated_by',
44
-    ];
45
-
46
-    protected $casts = [
47
-        'rate' => RateCast::class,
48
-        'computation' => DiscountComputation::class,
49
-        'type' => DiscountType::class,
50
-        'scope' => DiscountScope::class,
51
-        'start_date' => 'datetime',
52
-        'end_date' => 'datetime',
53
-        'enabled' => 'boolean',
54
-    ];
55
-
56
-    protected ?string $evaluatedDefault = 'type';
57
-
58
-    public function account(): BelongsTo
59
-    {
60
-        return $this->belongsTo(Account::class, 'account_id');
61
-    }
62
-
63
-    public function defaultSalesDiscount(): HasOne
64
-    {
65
-        return $this->hasOne(CompanyDefault::class, 'sales_discount_id');
66
-    }
67
-
68
-    public function defaultPurchaseDiscount(): HasOne
69
-    {
70
-        return $this->hasOne(CompanyDefault::class, 'purchase_discount_id');
71
-    }
72
-
73
-    public function adjustmentables(): MorphTo
74
-    {
75
-        return $this->morphTo();
76
-    }
77
-
78
-    protected static function newFactory(): Factory
79
-    {
80
-        return DiscountFactory::new();
81
-    }
82
-}

+ 0
- 78
app/Models/Setting/Tax.php 查看文件

1
-<?php
2
-
3
-namespace App\Models\Setting;
4
-
5
-use App\Casts\RateCast;
6
-use App\Concerns\Blamable;
7
-use App\Concerns\CompanyOwned;
8
-use App\Concerns\HasDefault;
9
-use App\Concerns\SyncsWithCompanyDefaults;
10
-use App\Enums\Setting\TaxComputation;
11
-use App\Enums\Setting\TaxScope;
12
-use App\Enums\Setting\TaxType;
13
-use App\Models\Accounting\Account;
14
-use Database\Factories\Setting\TaxFactory;
15
-use Illuminate\Database\Eloquent\Factories\Factory;
16
-use Illuminate\Database\Eloquent\Factories\HasFactory;
17
-use Illuminate\Database\Eloquent\Model;
18
-use Illuminate\Database\Eloquent\Relations\BelongsTo;
19
-use Illuminate\Database\Eloquent\Relations\HasOne;
20
-use Illuminate\Database\Eloquent\Relations\MorphTo;
21
-
22
-class Tax extends Model
23
-{
24
-    use Blamable;
25
-    use CompanyOwned;
26
-    use HasDefault;
27
-    use HasFactory;
28
-    use SyncsWithCompanyDefaults;
29
-
30
-    protected $table = 'taxes';
31
-
32
-    protected $fillable = [
33
-        'company_id',
34
-        'account_id',
35
-        'rate',
36
-        'computation',
37
-        'type',
38
-        'scope',
39
-        'enabled',
40
-        'created_by',
41
-        'updated_by',
42
-    ];
43
-
44
-    protected $casts = [
45
-        'rate' => RateCast::class,
46
-        'computation' => TaxComputation::class,
47
-        'type' => TaxType::class,
48
-        'scope' => TaxScope::class,
49
-        'enabled' => 'boolean',
50
-    ];
51
-
52
-    protected ?string $evaluatedDefault = 'type';
53
-
54
-    public function account(): BelongsTo
55
-    {
56
-        return $this->belongsTo(Account::class, 'account_id');
57
-    }
58
-
59
-    public function defaultSalesTax(): HasOne
60
-    {
61
-        return $this->hasOne(CompanyDefault::class, 'sales_tax_id');
62
-    }
63
-
64
-    public function defaultPurchaseTax(): HasOne
65
-    {
66
-        return $this->hasOne(CompanyDefault::class, 'purchase_tax_id');
67
-    }
68
-
69
-    public function adjustmentables(): MorphTo
70
-    {
71
-        return $this->morphTo();
72
-    }
73
-
74
-    protected static function newFactory(): Factory
75
-    {
76
-        return TaxFactory::new();
77
-    }
78
-}

+ 0
- 2
app/Providers/AuthServiceProvider.php 查看文件

36
     {
36
     {
37
         $models = [
37
         $models = [
38
             Setting\Currency::class,
38
             Setting\Currency::class,
39
-            Setting\Discount::class,
40
-            Setting\Tax::class,
41
             Banking\BankAccount::class,
39
             Banking\BankAccount::class,
42
         ];
40
         ];
43
 
41
 

+ 16
- 13
app/Providers/MacroServiceProvider.php 查看文件

19
 use Filament\Tables\Columns\TextColumn;
19
 use Filament\Tables\Columns\TextColumn;
20
 use Illuminate\Support\Carbon;
20
 use Illuminate\Support\Carbon;
21
 use Illuminate\Support\ServiceProvider;
21
 use Illuminate\Support\ServiceProvider;
22
-use Illuminate\Support\Str;
23
 
22
 
24
 class MacroServiceProvider extends ServiceProvider
23
 class MacroServiceProvider extends ServiceProvider
25
 {
24
 {
103
             return $this;
102
             return $this;
104
         });
103
         });
105
 
104
 
106
-        TextInput::macro('rate', function (string | Closure | null $computation = null): static {
107
-            $this->extraAttributes(['wire:key' => Str::random()])
108
-                ->prefix(static function (TextInput $component) use ($computation) {
109
-                    $computation = $component->evaluate($computation);
110
-
111
-                    return ratePrefix(computation: $computation);
112
-                })
113
-                ->suffix(static function (TextInput $component) use ($computation) {
114
-                    $computation = $component->evaluate($computation);
115
-
116
-                    return rateSuffix(computation: $computation);
117
-                })
105
+        TextInput::macro('rate', function (string | Closure | null $computation = null, bool $showAffix = true): static {
106
+            $this
107
+                ->when(
108
+                    $showAffix,
109
+                    fn (TextInput $component) => $component
110
+                        ->prefix(static function (TextInput $component) use ($computation) {
111
+                            $computation = $component->evaluate($computation);
112
+
113
+                            return ratePrefix(computation: $computation);
114
+                        })
115
+                        ->suffix(static function (TextInput $component) use ($computation) {
116
+                            $computation = $component->evaluate($computation);
117
+
118
+                            return rateSuffix(computation: $computation);
119
+                        })
120
+                )
118
                 ->mask(static function (TextInput $component) use ($computation) {
121
                 ->mask(static function (TextInput $component) use ($computation) {
119
                     $computation = $component->evaluate($computation);
122
                     $computation = $component->evaluate($computation);
120
 
123
 

+ 11
- 2
app/Services/ReportService.php 查看文件

177
 
177
 
178
             $accountTransactions = [];
178
             $accountTransactions = [];
179
             $currentBalance = $account->starting_balance;
179
             $currentBalance = $account->starting_balance;
180
+            $periodDebitTotal = 0;
181
+            $periodCreditTotal = 0;
180
 
182
 
181
             $accountTransactions[] = new AccountTransactionDTO(
183
             $accountTransactions[] = new AccountTransactionDTO(
182
                 id: null,
184
                 id: null,
192
             foreach ($account->journalEntries as $journalEntry) {
194
             foreach ($account->journalEntries as $journalEntry) {
193
                 $transaction = $journalEntry->transaction;
195
                 $transaction = $journalEntry->transaction;
194
                 $signedAmount = $journalEntry->signed_amount;
196
                 $signedAmount = $journalEntry->signed_amount;
197
+                $amount = $journalEntry->getRawOriginal('amount');
198
+
199
+                if ($journalEntry->type->isDebit()) {
200
+                    $periodDebitTotal += $amount;
201
+                } else {
202
+                    $periodCreditTotal += $amount;
203
+                }
195
 
204
 
196
                 if ($account->category->isNormalDebitBalance()) {
205
                 if ($account->category->isNormalDebitBalance()) {
197
                     $currentBalance += $signedAmount;
206
                     $currentBalance += $signedAmount;
219
                 id: null,
228
                 id: null,
220
                 date: 'Totals and Ending Balance',
229
                 date: 'Totals and Ending Balance',
221
                 description: '',
230
                 description: '',
222
-                debit: money($account->total_debit, $defaultCurrency)->format(),
223
-                credit: money($account->total_credit, $defaultCurrency)->format(),
231
+                debit: money($periodDebitTotal, $defaultCurrency)->format(),
232
+                credit: money($periodCreditTotal, $defaultCurrency)->format(),
224
                 balance: money($currentBalance, $defaultCurrency)->format(),
233
                 balance: money($currentBalance, $defaultCurrency)->format(),
225
                 type: null,
234
                 type: null,
226
                 tableAction: null
235
                 tableAction: null

+ 27
- 11
app/View/Models/BillTotalViewModel.php 查看文件

2
 
2
 
3
 namespace App\View\Models;
3
 namespace App\View\Models;
4
 
4
 
5
+use App\Enums\Accounting\AdjustmentComputation;
6
+use App\Enums\Accounting\DocumentDiscountMethod;
5
 use App\Models\Accounting\Adjustment;
7
 use App\Models\Accounting\Adjustment;
6
 use App\Models\Accounting\Bill;
8
 use App\Models\Accounting\Bill;
7
 use App\Utilities\Currency\CurrencyConverter;
9
 use App\Utilities\Currency\CurrencyConverter;
17
     {
19
     {
18
         $lineItems = collect($this->data['lineItems'] ?? []);
20
         $lineItems = collect($this->data['lineItems'] ?? []);
19
 
21
 
20
-        $subtotal = $lineItems->sum(function ($item) {
22
+        $subtotal = $lineItems->sum(static function ($item) {
21
             $quantity = max((float) ($item['quantity'] ?? 0), 0);
23
             $quantity = max((float) ($item['quantity'] ?? 0), 0);
22
             $unitPrice = max((float) ($item['unit_price'] ?? 0), 0);
24
             $unitPrice = max((float) ($item['unit_price'] ?? 0), 0);
23
 
25
 
37
             return $carry + $taxAmount;
39
             return $carry + $taxAmount;
38
         }, 0);
40
         }, 0);
39
 
41
 
40
-        $discountTotal = $lineItems->reduce(function ($carry, $item) {
41
-            $quantity = max((float) ($item['quantity'] ?? 0), 0);
42
-            $unitPrice = max((float) ($item['unit_price'] ?? 0), 0);
43
-            $purchaseDiscounts = $item['purchaseDiscounts'] ?? [];
44
-            $lineTotal = $quantity * $unitPrice;
42
+        // Calculate discount based on method
43
+        $discountMethod = DocumentDiscountMethod::parse($this->data['discount_method']) ?? DocumentDiscountMethod::PerLineItem;
45
 
44
 
46
-            $discountAmount = Adjustment::whereIn('id', $purchaseDiscounts)
47
-                ->pluck('rate')
48
-                ->sum(fn ($rate) => $lineTotal * ($rate / 100));
45
+        if ($discountMethod->isPerLineItem()) {
46
+            $discountTotal = $lineItems->reduce(function ($carry, $item) {
47
+                $quantity = max((float) ($item['quantity'] ?? 0), 0);
48
+                $unitPrice = max((float) ($item['unit_price'] ?? 0), 0);
49
+                $purchaseDiscounts = $item['purchaseDiscounts'] ?? [];
50
+                $lineTotal = $quantity * $unitPrice;
49
 
51
 
50
-            return $carry + $discountAmount;
51
-        }, 0);
52
+                $discountAmount = Adjustment::whereIn('id', $purchaseDiscounts)
53
+                    ->pluck('rate')
54
+                    ->sum(fn ($rate) => $lineTotal * ($rate / 100));
55
+
56
+                return $carry + $discountAmount;
57
+            }, 0);
58
+        } else {
59
+            $discountComputation = AdjustmentComputation::parse($this->data['discount_computation']) ?? AdjustmentComputation::Percentage;
60
+            $discountRate = (float) ($this->data['discount_rate'] ?? 0);
61
+
62
+            if ($discountComputation->isPercentage()) {
63
+                $discountTotal = $subtotal * ($discountRate / 100);
64
+            } else {
65
+                $discountTotal = $discountRate;
66
+            }
67
+        }
52
 
68
 
53
         $grandTotal = $subtotal + ($taxTotal - $discountTotal);
69
         $grandTotal = $subtotal + ($taxTotal - $discountTotal);
54
 
70
 

+ 27
- 11
app/View/Models/InvoiceTotalViewModel.php 查看文件

2
 
2
 
3
 namespace App\View\Models;
3
 namespace App\View\Models;
4
 
4
 
5
+use App\Enums\Accounting\AdjustmentComputation;
6
+use App\Enums\Accounting\DocumentDiscountMethod;
5
 use App\Models\Accounting\Adjustment;
7
 use App\Models\Accounting\Adjustment;
6
 use App\Models\Accounting\Invoice;
8
 use App\Models\Accounting\Invoice;
7
 use App\Utilities\Currency\CurrencyConverter;
9
 use App\Utilities\Currency\CurrencyConverter;
17
     {
19
     {
18
         $lineItems = collect($this->data['lineItems'] ?? []);
20
         $lineItems = collect($this->data['lineItems'] ?? []);
19
 
21
 
20
-        $subtotal = $lineItems->sum(function ($item) {
22
+        $subtotal = $lineItems->sum(static function ($item) {
21
             $quantity = max((float) ($item['quantity'] ?? 0), 0);
23
             $quantity = max((float) ($item['quantity'] ?? 0), 0);
22
             $unitPrice = max((float) ($item['unit_price'] ?? 0), 0);
24
             $unitPrice = max((float) ($item['unit_price'] ?? 0), 0);
23
 
25
 
37
             return $carry + $taxAmount;
39
             return $carry + $taxAmount;
38
         }, 0);
40
         }, 0);
39
 
41
 
40
-        $discountTotal = $lineItems->reduce(function ($carry, $item) {
41
-            $quantity = max((float) ($item['quantity'] ?? 0), 0);
42
-            $unitPrice = max((float) ($item['unit_price'] ?? 0), 0);
43
-            $salesDiscounts = $item['salesDiscounts'] ?? [];
44
-            $lineTotal = $quantity * $unitPrice;
42
+        // Calculate discount based on method
43
+        $discountMethod = DocumentDiscountMethod::parse($this->data['discount_method']) ?? DocumentDiscountMethod::PerLineItem;
45
 
44
 
46
-            $discountAmount = Adjustment::whereIn('id', $salesDiscounts)
47
-                ->pluck('rate')
48
-                ->sum(fn ($rate) => $lineTotal * ($rate / 100));
45
+        if ($discountMethod->isPerLineItem()) {
46
+            $discountTotal = $lineItems->reduce(function ($carry, $item) {
47
+                $quantity = max((float) ($item['quantity'] ?? 0), 0);
48
+                $unitPrice = max((float) ($item['unit_price'] ?? 0), 0);
49
+                $salesDiscounts = $item['salesDiscounts'] ?? [];
50
+                $lineTotal = $quantity * $unitPrice;
49
 
51
 
50
-            return $carry + $discountAmount;
51
-        }, 0);
52
+                $discountAmount = Adjustment::whereIn('id', $salesDiscounts)
53
+                    ->pluck('rate')
54
+                    ->sum(fn ($rate) => $lineTotal * ($rate / 100));
55
+
56
+                return $carry + $discountAmount;
57
+            }, 0);
58
+        } else {
59
+            $discountComputation = AdjustmentComputation::parse($this->data['discount_computation']) ?? AdjustmentComputation::Percentage;
60
+            $discountRate = (float) ($this->data['discount_rate'] ?? 0);
61
+
62
+            if ($discountComputation->isPercentage()) {
63
+                $discountTotal = $subtotal * ($discountRate / 100);
64
+            } else {
65
+                $discountTotal = $discountRate;
66
+            }
67
+        }
52
 
68
 
53
         $grandTotal = $subtotal + ($taxTotal - $discountTotal);
69
         $grandTotal = $subtotal + ($taxTotal - $discountTotal);
54
 
70
 

+ 1
- 1
composer.json 查看文件

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

+ 313
- 376
composer.lock
文件差異過大導致無法顯示
查看文件


+ 3
- 1
database/factories/Accounting/AdjustmentFactory.php 查看文件

30
     {
30
     {
31
         $startDate = $this->faker->dateTimeBetween('now', '+1 year');
31
         $startDate = $this->faker->dateTimeBetween('now', '+1 year');
32
         $endDate = $this->faker->dateTimeBetween($startDate, Carbon::parse($startDate)->addYear());
32
         $endDate = $this->faker->dateTimeBetween($startDate, Carbon::parse($startDate)->addYear());
33
+
34
+        /** @var AdjustmentComputation $computation */
33
         $computation = $this->faker->randomElement(AdjustmentComputation::class);
35
         $computation = $this->faker->randomElement(AdjustmentComputation::class);
34
 
36
 
35
-        $rate = $computation === AdjustmentComputation::Fixed
37
+        $rate = $computation->isFixed()
36
             ? $this->faker->numberBetween(5, 100) * 100 // $5 - $100 for fixed amounts
38
             ? $this->faker->numberBetween(5, 100) * 100 // $5 - $100 for fixed amounts
37
             : $this->faker->numberBetween(3, 25) * 10000; // 3% - 25% for percentages
39
             : $this->faker->numberBetween(3, 25) * 10000; // 3% - 25% for percentages
38
 
40
 

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

22
             $table->timestamp('paid_at')->nullable();
22
             $table->timestamp('paid_at')->nullable();
23
             $table->string('status')->default('unpaid');
23
             $table->string('status')->default('unpaid');
24
             $table->string('currency_code')->nullable();
24
             $table->string('currency_code')->nullable();
25
+            $table->string('discount_method')->default('per_line_item');
26
+            $table->string('discount_computation')->default('percentage');
27
+            $table->integer('discount_rate')->default(0);
25
             $table->integer('subtotal')->default(0);
28
             $table->integer('subtotal')->default(0);
26
             $table->integer('tax_total')->default(0);
29
             $table->integer('tax_total')->default(0);
27
             $table->integer('discount_total')->default(0);
30
             $table->integer('discount_total')->default(0);

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

27
             $table->timestamp('last_sent')->nullable();
27
             $table->timestamp('last_sent')->nullable();
28
             $table->string('status')->default('draft');
28
             $table->string('status')->default('draft');
29
             $table->string('currency_code')->nullable();
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);
30
             $table->integer('subtotal')->default(0);
33
             $table->integer('subtotal')->default(0);
31
             $table->integer('tax_total')->default(0);
34
             $table->integer('tax_total')->default(0);
32
             $table->integer('discount_total')->default(0);
35
             $table->integer('discount_total')->default(0);

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

456
             }
456
             }
457
         },
457
         },
458
         "node_modules/@jridgewell/gen-mapping": {
458
         "node_modules/@jridgewell/gen-mapping": {
459
-            "version": "0.3.5",
460
-            "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
461
-            "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
459
+            "version": "0.3.8",
460
+            "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
461
+            "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
462
             "dev": true,
462
             "dev": true,
463
             "license": "MIT",
463
             "license": "MIT",
464
             "dependencies": {
464
             "dependencies": {
1014
             }
1014
             }
1015
         },
1015
         },
1016
         "node_modules/browserslist": {
1016
         "node_modules/browserslist": {
1017
-            "version": "4.24.2",
1018
-            "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz",
1019
-            "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==",
1017
+            "version": "4.24.3",
1018
+            "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz",
1019
+            "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==",
1020
             "dev": true,
1020
             "dev": true,
1021
             "funding": [
1021
             "funding": [
1022
                 {
1022
                 {
1034
             ],
1034
             ],
1035
             "license": "MIT",
1035
             "license": "MIT",
1036
             "dependencies": {
1036
             "dependencies": {
1037
-                "caniuse-lite": "^1.0.30001669",
1038
-                "electron-to-chromium": "^1.5.41",
1039
-                "node-releases": "^2.0.18",
1037
+                "caniuse-lite": "^1.0.30001688",
1038
+                "electron-to-chromium": "^1.5.73",
1039
+                "node-releases": "^2.0.19",
1040
                 "update-browserslist-db": "^1.1.1"
1040
                 "update-browserslist-db": "^1.1.1"
1041
             },
1041
             },
1042
             "bin": {
1042
             "bin": {
1057
             }
1057
             }
1058
         },
1058
         },
1059
         "node_modules/caniuse-lite": {
1059
         "node_modules/caniuse-lite": {
1060
-            "version": "1.0.30001687",
1061
-            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001687.tgz",
1062
-            "integrity": "sha512-0S/FDhf4ZiqrTUiQ39dKeUjYRjkv7lOZU1Dgif2rIqrTzX/1wV2hfKu9TOm1IHkdSijfLswxTFzl/cvir+SLSQ==",
1060
+            "version": "1.0.30001688",
1061
+            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001688.tgz",
1062
+            "integrity": "sha512-Nmqpru91cuABu/DTCXbM2NSRHzM2uVHfPnhJ/1zEAJx/ILBRVmz3pzH4N7DZqbdG0gWClsCC05Oj0mJ/1AWMbA==",
1063
             "dev": true,
1063
             "dev": true,
1064
             "funding": [
1064
             "funding": [
1065
                 {
1065
                 {
1218
             "license": "MIT"
1218
             "license": "MIT"
1219
         },
1219
         },
1220
         "node_modules/electron-to-chromium": {
1220
         "node_modules/electron-to-chromium": {
1221
-            "version": "1.5.71",
1222
-            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.71.tgz",
1223
-            "integrity": "sha512-dB68l59BI75W1BUGVTAEJy45CEVuEGy9qPVVQ8pnHyHMn36PLPPoE1mjLH+lo9rKulO3HC2OhbACI/8tCqJBcA==",
1221
+            "version": "1.5.73",
1222
+            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.73.tgz",
1223
+            "integrity": "sha512-8wGNxG9tAG5KhGd3eeA0o6ixhiNdgr0DcHWm85XPCphwZgD1lIEoi6t3VERayWao7SF7AAZTw6oARGJeVjH8Kg==",
1224
             "dev": true,
1224
             "dev": true,
1225
             "license": "ISC"
1225
             "license": "ISC"
1226
         },
1226
         },
1487
             }
1487
             }
1488
         },
1488
         },
1489
         "node_modules/is-core-module": {
1489
         "node_modules/is-core-module": {
1490
-            "version": "2.15.1",
1491
-            "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz",
1492
-            "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==",
1490
+            "version": "2.16.0",
1491
+            "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.0.tgz",
1492
+            "integrity": "sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g==",
1493
             "dev": true,
1493
             "dev": true,
1494
             "license": "MIT",
1494
             "license": "MIT",
1495
             "dependencies": {
1495
             "dependencies": {
1761
             }
1761
             }
1762
         },
1762
         },
1763
         "node_modules/node-releases": {
1763
         "node_modules/node-releases": {
1764
-            "version": "2.0.18",
1765
-            "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
1766
-            "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
1764
+            "version": "2.0.19",
1765
+            "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
1766
+            "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
1767
             "dev": true,
1767
             "dev": true,
1768
             "license": "MIT"
1768
             "license": "MIT"
1769
         },
1769
         },
2192
             }
2192
             }
2193
         },
2193
         },
2194
         "node_modules/resolve": {
2194
         "node_modules/resolve": {
2195
-            "version": "1.22.8",
2196
-            "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
2197
-            "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==",
2195
+            "version": "1.22.9",
2196
+            "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.9.tgz",
2197
+            "integrity": "sha512-QxrmX1DzraFIi9PxdG5VkRfRwIgjwyud+z/iBwfRRrVmHc+P9Q7u2lSSpQ6bjr2gy5lrqIiU9vb6iAeGf2400A==",
2198
             "dev": true,
2198
             "dev": true,
2199
             "license": "MIT",
2199
             "license": "MIT",
2200
             "dependencies": {
2200
             "dependencies": {
2201
-                "is-core-module": "^2.13.0",
2201
+                "is-core-module": "^2.16.0",
2202
                 "path-parse": "^1.0.7",
2202
                 "path-parse": "^1.0.7",
2203
                 "supports-preserve-symlinks-flag": "^1.0.0"
2203
                 "supports-preserve-symlinks-flag": "^1.0.0"
2204
             },
2204
             },

+ 11
- 1
resources/views/components/company/invoice/container.blade.php 查看文件

1
+@props([
2
+    'preview' => false,
3
+])
4
+
1
 <div class="inv-container flex justify-center p-6">
5
 <div class="inv-container flex justify-center p-6">
2
-    <div class="inv-paper bg-[#ffffff] dark:bg-gray-800 rounded-sm shadow-xl w-[38.25rem] h-[49.5rem] overflow-hidden">
6
+    <div
7
+        @class([
8
+            'inv-paper bg-[#ffffff] dark:bg-gray-800 rounded-sm shadow-xl',
9
+            'w-full max-w-[820px] min-h-[1024px]' => $preview === false,
10
+            'w-[38.25rem] h-[49.5rem] overflow-hidden' => $preview === true,
11
+        ])
12
+    >
3
         {{ $slot }}
13
         {{ $slot }}
4
     </div>
14
     </div>
5
 </div>
15
 </div>

+ 1
- 1
resources/views/components/company/invoice/footer.blade.php 查看文件

1
-<footer {{ $attributes->class(['inv-footer text-xs text-gray-600 dark:text-gray-300']) }}>
1
+<footer {{ $attributes->class(['inv-footer text-xs text-gray-600 dark:text-gray-300 min-h-60']) }}>
2
     {{ $slot }}
2
     {{ $slot }}
3
 </footer>
3
 </footer>

+ 1
- 1
resources/views/filament/company/components/invoice-layouts/classic.blade.php 查看文件

13
     }
13
     }
14
 </style>
14
 </style>
15
 
15
 
16
-<x-company.invoice.container class="classic-template-container">
16
+<x-company.invoice.container class="classic-template-container" preview>
17
     <!-- Header Section -->
17
     <!-- Header Section -->
18
     <x-company.invoice.header class="default-template-header">
18
     <x-company.invoice.header class="default-template-header">
19
         <div class="w-2/3 text-left ml-6">
19
         <div class="w-2/3 text-left ml-6">

+ 1
- 1
resources/views/filament/company/components/invoice-layouts/default.blade.php 查看文件

13
     }
13
     }
14
 </style>
14
 </style>
15
 
15
 
16
-<x-company.invoice.container class="default-template-container">
16
+<x-company.invoice.container class="default-template-container" preview>
17
 
17
 
18
     <x-company.invoice.header class="default-template-header border-b-2 p-6 pb-4">
18
     <x-company.invoice.header class="default-template-header border-b-2 p-6 pb-4">
19
         <div class="w-2/3">
19
         <div class="w-2/3">

+ 1
- 1
resources/views/filament/company/components/invoice-layouts/modern.blade.php 查看文件

13
     }
13
     }
14
 </style>
14
 </style>
15
 
15
 
16
-<x-company.invoice.container class="modern-template-container">
16
+<x-company.invoice.container class="modern-template-container" preview>
17
 
17
 
18
     <!-- Colored Header with Logo -->
18
     <!-- Colored Header with Logo -->
19
     <x-company.invoice.header class="bg-gray-800 h-20">
19
     <x-company.invoice.header class="bg-gray-800 h-20">

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

1
+@php
2
+    /** @var \App\Models\Accounting\Invoice $invoice */
3
+    $invoiceSettings = $invoice->company->defaultInvoice;
4
+
5
+    $company = $invoice->company;
6
+
7
+    use App\Utilities\Currency\CurrencyConverter;
8
+@endphp
9
+
10
+<x-company.invoice.container class="modern-template-container">
11
+    <!-- Colored Header with Logo -->
12
+    <x-company.invoice.header class="bg-gray-800 h-24">
13
+        <!-- Logo -->
14
+        <div class="w-2/3">
15
+            @if($invoice->logo && $invoiceSettings->show_logo)
16
+                <x-company.invoice.logo class="ml-8" :src="$invoice->logo"/>
17
+            @endif
18
+        </div>
19
+
20
+        <!-- Ribbon Container -->
21
+        <div class="w-1/3 absolute right-0 top-0 p-3 h-32 flex flex-col justify-end rounded-bl-sm"
22
+             style="background: {{ $invoiceSettings->accent_color }};">
23
+            @if($invoice->header)
24
+                <h1 class="text-4xl font-bold text-white text-center uppercase">{{ $invoice->header }}</h1>
25
+            @endif
26
+        </div>
27
+    </x-company.invoice.header>
28
+
29
+    <!-- Company Details -->
30
+    <x-company.invoice.metadata class="modern-template-metadata space-y-8">
31
+        <div class="text-sm">
32
+            <h2 class="text-lg font-semibold">{{ $company->name }}</h2>
33
+            @if($company->profile->address && $company->profile->city?->name && $company->profile->state?->name && $company->profile?->zip_code)
34
+                <p>{{ $company->profile->address }}</p>
35
+                <p>{{ $company->profile->city->name }}
36
+                    , {{ $company->profile->state->name }} {{ $company->profile->zip_code }}</p>
37
+                <p>{{ $company->profile->state->country->name }}</p>
38
+            @endif
39
+        </div>
40
+
41
+        <div class="flex justify-between items-end">
42
+            <!-- Billing Details -->
43
+            <div class="text-sm tracking-tight">
44
+                <h3 class="text-gray-600 dark:text-gray-400 font-medium tracking-tight mb-1">BILL TO</h3>
45
+                <p class="text-base font-bold"
46
+                   style="color: {{ $invoiceSettings->accent_color }}">{{ $invoice->client->name }}</p>
47
+
48
+                @if($invoice->client->billingAddress)
49
+                    @php
50
+                        $address = $invoice->client->billingAddress;
51
+                    @endphp
52
+                    @if($address->address_line_1)
53
+                        <p>{{ $address->address_line_1 }}</p>
54
+                    @endif
55
+                    @if($address->address_line_2)
56
+                        <p>{{ $address->address_line_2 }}</p>
57
+                    @endif
58
+                    <p>
59
+                        {{ $address->city }}{{ $address->state ? ', ' . $address->state : '' }}
60
+                        {{ $address->postal_code }}
61
+                    </p>
62
+                    @if($address->country)
63
+                        <p>{{ $address->country }}</p>
64
+                    @endif
65
+                @endif
66
+            </div>
67
+
68
+            <div class="text-sm tracking-tight">
69
+                <table class="min-w-full">
70
+                    <tbody>
71
+                    <tr>
72
+                        <td class="font-semibold text-right pr-2">Invoice Number:</td>
73
+                        <td class="text-left pl-2">{{ $invoice->invoice_number }}</td>
74
+                    </tr>
75
+                    @if($invoice->order_number)
76
+                        <tr>
77
+                            <td class="font-semibold text-right pr-2">P.O/S.O Number:</td>
78
+                            <td class="text-left pl-2">{{ $invoice->order_number }}</td>
79
+                        </tr>
80
+                    @endif
81
+                    <tr>
82
+                        <td class="font-semibold text-right pr-2">Invoice Date:</td>
83
+                        <td class="text-left pl-2">{{ $invoice->date->toDefaultDateFormat() }}</td>
84
+                    </tr>
85
+                    <tr>
86
+                        <td class="font-semibold text-right pr-2">Payment Due:</td>
87
+                        <td class="text-left pl-2">{{ $invoice->due_date->toDefaultDateFormat() }}</td>
88
+                    </tr>
89
+                    </tbody>
90
+                </table>
91
+            </div>
92
+        </div>
93
+    </x-company.invoice.metadata>
94
+
95
+    <!-- Line Items Table -->
96
+    <x-company.invoice.line-items class="modern-template-line-items">
97
+        <table class="w-full text-left table-fixed">
98
+            <thead class="text-sm leading-relaxed">
99
+            <tr class="text-gray-600 dark:text-gray-400">
100
+                <th class="text-left pl-6 w-[45%] py-4">Items</th>
101
+                <th class="text-center w-[15%] py-4">Quantity</th>
102
+                <th class="text-right w-[20%] py-4">Price</th>
103
+                <th class="text-right pr-6 w-[20%] py-4">Amount</th>
104
+            </tr>
105
+            </thead>
106
+            <tbody class="text-sm tracking-tight border-y-2">
107
+            @foreach($invoice->lineItems as $index => $item)
108
+                <tr @class(['bg-gray-100 dark:bg-gray-800' => $index % 2 === 0])>
109
+                    <td class="text-left pl-6 font-semibold py-3">
110
+                        {{ $item->offering->name }}
111
+                        @if($item->description)
112
+                            <div class="text-gray-600 font-normal line-clamp-2 mt-1">{{ $item->description }}</div>
113
+                        @endif
114
+                    </td>
115
+                    <td class="text-center py-3">{{ $item->quantity }}</td>
116
+                    <td class="text-right py-3">{{ CurrencyConverter::formatToMoney($item->unit_price) }}</td>
117
+                    <td class="text-right pr-6 py-3">{{ CurrencyConverter::formatToMoney($item->subtotal) }}</td>
118
+                </tr>
119
+            @endforeach
120
+            </tbody>
121
+            <tfoot class="text-sm tracking-tight">
122
+            <tr>
123
+                <td class="pl-6 py-2" colspan="2"></td>
124
+                <td class="text-right font-semibold py-2">Subtotal:</td>
125
+                <td class="text-right pr-6 py-2">{{ CurrencyConverter::formatToMoney($invoice->subtotal) }}</td>
126
+            </tr>
127
+            @if($invoice->discount_total)
128
+                <tr class="text-success-800 dark:text-success-600">
129
+                    <td class="pl-6 py-2" colspan="2"></td>
130
+                    <td class="text-right py-2">Discount:</td>
131
+                    <td class="text-right pr-6 py-2">
132
+                        ({{ CurrencyConverter::formatToMoney($invoice->discount_total) }})
133
+                    </td>
134
+                </tr>
135
+            @endif
136
+            @if($invoice->tax_total)
137
+                <tr>
138
+                    <td class="pl-6 py-2" colspan="2"></td>
139
+                    <td class="text-right py-2">Tax:</td>
140
+                    <td class="text-right pr-6 py-2">{{ CurrencyConverter::formatToMoney($invoice->tax_total) }}</td>
141
+                </tr>
142
+            @endif
143
+            <tr>
144
+                <td class="pl-6 py-2" colspan="2"></td>
145
+                <td class="text-right font-semibold border-t py-2">Total:</td>
146
+                <td class="text-right border-t pr-6 py-2">{{ CurrencyConverter::formatToMoney($invoice->total) }}</td>
147
+            </tr>
148
+            <tr>
149
+                <td class="pl-6 py-2" colspan="2"></td>
150
+                <td class="text-right font-semibold border-t-4 border-double py-2">Amount Due
151
+                    ({{ $invoice->currency_code }}):
152
+                </td>
153
+                <td class="text-right border-t-4 border-double pr-6 py-2">{{ CurrencyConverter::formatToMoney($invoice->amount_due) }}</td>
154
+            </tr>
155
+            </tfoot>
156
+        </table>
157
+    </x-company.invoice.line-items>
158
+
159
+    <!-- Footer Notes -->
160
+    <x-company.invoice.footer class="modern-template-footer tracking-tight">
161
+        <h4 class="font-semibold px-6 text-sm" style="color: {{ $invoiceSettings->accent_color }}">Terms &
162
+            Conditions</h4>
163
+        <span class="border-t-2 my-2 border-gray-300 block w-full"></span>
164
+        <div class="flex justify-between space-x-4 px-6 text-sm">
165
+            <p class="w-1/2 break-words line-clamp-4">{{ $invoice->terms }}</p>
166
+            <p class="w-1/2 break-words line-clamp-4">{{ $invoice->footer }}</p>
167
+        </div>
168
+    </x-company.invoice.footer>
169
+</x-company.invoice.container>

+ 47
- 0
resources/views/filament/company/resources/sales/invoices/pages/view-invoice.blade.php 查看文件

1
+<x-filament-panels::page
2
+    @class([
3
+        'fi-resource-view-record-page',
4
+        'fi-resource-' . str_replace('/', '-', $this->getResource()::getSlug()),
5
+        'fi-resource-record-' . $record->getKey(),
6
+    ])
7
+>
8
+    @php
9
+        $relationManagers = $this->getRelationManagers();
10
+        $hasCombinedRelationManagerTabsWithContent = $this->hasCombinedRelationManagerTabsWithContent();
11
+    @endphp
12
+
13
+    @if ((! $hasCombinedRelationManagerTabsWithContent) || (! count($relationManagers)))
14
+        @if ($this->hasInfolist())
15
+            {{ $this->infolist }}
16
+        @else
17
+            <div
18
+                wire:key="{{ $this->getId() }}.forms.{{ $this->getFormStatePath() }}"
19
+            >
20
+                {{ $this->form }}
21
+            </div>
22
+        @endif
23
+    @endif
24
+
25
+    @if (count($relationManagers))
26
+        <x-filament-panels::resources.relation-managers
27
+            :active-locale="isset($activeLocale) ? $activeLocale : null"
28
+            :active-manager="$this->activeRelationManager ?? ($hasCombinedRelationManagerTabsWithContent ? null : array_key_first($relationManagers))"
29
+            :content-tab-label="$this->getContentTabLabel()"
30
+            :content-tab-icon="$this->getContentTabIcon()"
31
+            :content-tab-position="$this->getContentTabPosition()"
32
+            :managers="$relationManagers"
33
+            :owner-record="$record"
34
+            :page-class="static::class"
35
+        >
36
+            @if ($hasCombinedRelationManagerTabsWithContent)
37
+                <x-slot name="content">
38
+                    @if ($this->hasInfolist())
39
+                        {{ $this->infolist }}
40
+                    @else
41
+                        {{ $this->form }}
42
+                    @endif
43
+                </x-slot>
44
+            @endif
45
+        </x-filament-panels::resources.relation-managers>
46
+    @endif
47
+</x-filament-panels::page>

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

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

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

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

Loading…
取消
儲存