Andrew Wallo 10 months ago
parent
commit
6cec455b7e

+ 13
- 7
app/Concerns/ManagesLineItems.php View File

4
 
4
 
5
 use App\Enums\Accounting\AdjustmentComputation;
5
 use App\Enums\Accounting\AdjustmentComputation;
6
 use App\Enums\Accounting\DocumentDiscountMethod;
6
 use App\Enums\Accounting\DocumentDiscountMethod;
7
+use App\Models\Accounting\Bill;
7
 use App\Models\Accounting\DocumentLineItem;
8
 use App\Models\Accounting\DocumentLineItem;
8
-use App\Models\Accounting\Invoice;
9
 use App\Utilities\Currency\CurrencyConverter;
9
 use App\Utilities\Currency\CurrencyConverter;
10
+use Illuminate\Database\Eloquent\Model;
10
 use Illuminate\Support\Collection;
11
 use Illuminate\Support\Collection;
11
 
12
 
12
 trait ManagesLineItems
13
 trait ManagesLineItems
13
 {
14
 {
14
-    protected function handleLineItems(Invoice $record, Collection $lineItems): void
15
+    protected function handleLineItems(Model $record, Collection $lineItems): void
15
     {
16
     {
16
         foreach ($lineItems as $itemData) {
17
         foreach ($lineItems as $itemData) {
17
             $lineItem = isset($itemData['id'])
18
             $lineItem = isset($itemData['id'])
36
         }
37
         }
37
     }
38
     }
38
 
39
 
39
-    protected function deleteRemovedLineItems(Invoice $record, Collection $lineItems): void
40
+    protected function deleteRemovedLineItems(Model $record, Collection $lineItems): void
40
     {
41
     {
41
         $existingLineItemIds = $record->lineItems->pluck('id');
42
         $existingLineItemIds = $record->lineItems->pluck('id');
42
         $updatedLineItemIds = $lineItems->pluck('id')->filter();
43
         $updatedLineItemIds = $lineItems->pluck('id')->filter();
52
 
53
 
53
     protected function handleLineItemAdjustments(DocumentLineItem $lineItem, array $itemData, DocumentDiscountMethod $discountMethod): void
54
     protected function handleLineItemAdjustments(DocumentLineItem $lineItem, array $itemData, DocumentDiscountMethod $discountMethod): void
54
     {
55
     {
55
-        $adjustmentIds = collect($itemData['salesTaxes'] ?? [])
56
-            ->merge($discountMethod->isPerLineItem() ? ($itemData['salesDiscounts'] ?? []) : [])
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] ?? []) : [])
57
             ->filter()
63
             ->filter()
58
             ->unique();
64
             ->unique();
59
 
65
 
71
         ]);
77
         ]);
72
     }
78
     }
73
 
79
 
74
-    protected function updateInvoiceTotals(Invoice $record, array $data): array
80
+    protected function updateDocumentTotals(Model $record, array $data): array
75
     {
81
     {
76
         $subtotalCents = $record->lineItems()->sum('subtotal');
82
         $subtotalCents = $record->lineItems()->sum('subtotal');
77
         $taxTotalCents = $record->lineItems()->sum('tax_total');
83
         $taxTotalCents = $record->lineItems()->sum('tax_total');
98
         ?AdjustmentComputation $discountComputation,
104
         ?AdjustmentComputation $discountComputation,
99
         ?string $discountRate,
105
         ?string $discountRate,
100
         int $subtotalCents,
106
         int $subtotalCents,
101
-        Invoice $record
107
+        Model $record
102
     ): int {
108
     ): int {
103
         if ($discountMethod->isPerLineItem()) {
109
         if ($discountMethod->isPerLineItem()) {
104
             return $record->lineItems()->sum('discount_total');
110
             return $record->lineItems()->sum('discount_total');

+ 52
- 125
app/Filament/Company/Resources/Purchases/BillResource.php View File

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 View File

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 View File

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
 }

+ 1
- 1
app/Filament/Company/Resources/Sales/InvoiceResource/Pages/CreateInvoice.php View File

29
 
29
 
30
         $this->handleLineItems($record, collect($data['lineItems'] ?? []));
30
         $this->handleLineItems($record, collect($data['lineItems'] ?? []));
31
 
31
 
32
-        $totals = $this->updateInvoiceTotals($record, $data);
32
+        $totals = $this->updateDocumentTotals($record, $data);
33
 
33
 
34
         $record->updateQuietly($totals);
34
         $record->updateQuietly($totals);
35
 
35
 

+ 1
- 1
app/Filament/Company/Resources/Sales/InvoiceResource/Pages/EditInvoice.php View File

39
 
39
 
40
         $this->handleLineItems($record, $lineItems);
40
         $this->handleLineItems($record, $lineItems);
41
 
41
 
42
-        $totals = $this->updateInvoiceTotals($record, $data);
42
+        $totals = $this->updateDocumentTotals($record, $data);
43
 
43
 
44
         $data = array_merge($data, $totals);
44
         $data = array_merge($data, $totals);
45
 
45
 

+ 37
- 0
app/Filament/Forms/Components/BillTotals.php View File

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
+}

+ 5
- 0
app/Models/Accounting/Account.php View File

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

+ 29
- 1
app/Models/Accounting/Bill.php View File

14
 use App\Filament\Company\Resources\Purchases\BillResource;
14
 use App\Filament\Company\Resources\Purchases\BillResource;
15
 use App\Models\Common\Vendor;
15
 use App\Models\Common\Vendor;
16
 use App\Observers\BillObserver;
16
 use App\Observers\BillObserver;
17
+use App\Utilities\Currency\CurrencyConverter;
17
 use Filament\Actions\MountableAction;
18
 use Filament\Actions\MountableAction;
18
 use Filament\Actions\ReplicateAction;
19
 use Filament\Actions\ReplicateAction;
19
 use Illuminate\Database\Eloquent\Attributes\ObservedBy;
20
 use Illuminate\Database\Eloquent\Attributes\ObservedBy;
224
             'description' => $baseDescription,
225
             'description' => $baseDescription,
225
         ]);
226
         ]);
226
 
227
 
227
-        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) {
228
             $lineItemDescription = "{$baseDescription} › {$lineItem->offering->name}";
233
             $lineItemDescription = "{$baseDescription} › {$lineItem->offering->name}";
229
 
234
 
230
             $transaction->journalEntries()->create([
235
             $transaction->journalEntries()->create([
254
                     ]);
259
                     ]);
255
                 }
260
                 }
256
             }
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
+            }
257
         }
285
         }
258
     }
286
     }
259
 
287
 

+ 27
- 11
app/View/Models/BillTotalViewModel.php View File

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
 

+ 45
- 18
resources/views/filament/forms/components/bill-totals.blade.php View File

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
-

Loading…
Cancel
Save