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

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

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

+ 0
- 9
app/Enums/Accounting/DocumentType.php 查看文件

1
-<?php
2
-
3
-namespace App\Enums\Accounting;
4
-
5
-enum DocumentType: string
6
-{
7
-    case Invoice = 'invoice';
8
-    case Bill = 'bill';
9
-}

+ 270
- 2
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\Filament\Company\Resources\Purchases\BillResource\Pages;
5
 use App\Filament\Company\Resources\Purchases\BillResource\Pages;
6
+use App\Models\Accounting\Adjustment;
6
 use App\Models\Accounting\Bill;
7
 use App\Models\Accounting\Bill;
8
+use App\Models\Accounting\DocumentLineItem;
9
+use App\Models\Common\Offering;
10
+use App\Utilities\Currency\CurrencyAccessor;
11
+use Awcodes\TableRepeater\Components\TableRepeater;
12
+use Awcodes\TableRepeater\Header;
13
+use Carbon\CarbonInterface;
14
+use Filament\Forms;
7
 use Filament\Forms\Form;
15
 use Filament\Forms\Form;
8
 use Filament\Resources\Resource;
16
 use Filament\Resources\Resource;
17
+use Filament\Tables;
9
 use Filament\Tables\Table;
18
 use Filament\Tables\Table;
19
+use Illuminate\Database\Eloquent\Model;
20
+use Illuminate\Support\Carbon;
21
+use Illuminate\Support\Facades\Auth;
10
 
22
 
11
 class BillResource extends Resource
23
 class BillResource extends Resource
12
 {
24
 {
16
 
28
 
17
     public static function form(Form $form): Form
29
     public static function form(Form $form): Form
18
     {
30
     {
31
+        $company = Auth::user()->currentCompany;
32
+
19
         return $form
33
         return $form
20
             ->schema([
34
             ->schema([
21
-                //
35
+                Forms\Components\Section::make('Bill Details')
36
+                    ->schema([
37
+                        Forms\Components\Split::make([
38
+                            Forms\Components\Group::make([
39
+                                Forms\Components\Select::make('vendor_id')
40
+                                    ->relationship('vendor', 'name')
41
+                                    ->preload()
42
+                                    ->searchable()
43
+                                    ->required(),
44
+                            ]),
45
+                            Forms\Components\Group::make([
46
+                                Forms\Components\TextInput::make('bill_number')
47
+                                    ->label('Bill Number')
48
+                                    ->default(fn () => Bill::getNextDocumentNumber()),
49
+                                Forms\Components\TextInput::make('order_number')
50
+                                    ->label('P.O/S.O Number'),
51
+                                Forms\Components\DatePicker::make('date')
52
+                                    ->label('Bill Date')
53
+                                    ->default(now()),
54
+                                Forms\Components\DatePicker::make('due_date')
55
+                                    ->label('Due Date')
56
+                                    ->default(function () use ($company) {
57
+                                        return now()->addDays($company->defaultBill->payment_terms->getDays());
58
+                                    }),
59
+                            ])->grow(true),
60
+                        ])->from('md'),
61
+                        TableRepeater::make('lineItems')
62
+                            ->relationship()
63
+                            ->saveRelationshipsUsing(function (TableRepeater $component, Forms\Contracts\HasForms $livewire, ?array $state) {
64
+                                if (! is_array($state)) {
65
+                                    $state = [];
66
+                                }
67
+
68
+                                $relationship = $component->getRelationship();
69
+
70
+                                $existingRecords = $component->getCachedExistingRecords();
71
+
72
+                                $recordsToDelete = [];
73
+
74
+                                foreach ($existingRecords->pluck($relationship->getRelated()->getKeyName()) as $keyToCheckForDeletion) {
75
+                                    if (array_key_exists("record-{$keyToCheckForDeletion}", $state)) {
76
+                                        continue;
77
+                                    }
78
+
79
+                                    $recordsToDelete[] = $keyToCheckForDeletion;
80
+                                    $existingRecords->forget("record-{$keyToCheckForDeletion}");
81
+                                }
82
+
83
+                                $relationship
84
+                                    ->whereKey($recordsToDelete)
85
+                                    ->get()
86
+                                    ->each(static fn (Model $record) => $record->delete());
87
+
88
+                                $childComponentContainers = $component->getChildComponentContainers(
89
+                                    withHidden: $component->shouldSaveRelationshipsWhenHidden(),
90
+                                );
91
+
92
+                                $itemOrder = 1;
93
+                                $orderColumn = $component->getOrderColumn();
94
+
95
+                                $translatableContentDriver = $livewire->makeFilamentTranslatableContentDriver();
96
+
97
+                                foreach ($childComponentContainers as $itemKey => $item) {
98
+                                    $itemData = $item->getState(shouldCallHooksBefore: false);
99
+
100
+                                    if ($orderColumn) {
101
+                                        $itemData[$orderColumn] = $itemOrder;
102
+
103
+                                        $itemOrder++;
104
+                                    }
105
+
106
+                                    if ($record = ($existingRecords[$itemKey] ?? null)) {
107
+                                        $itemData = $component->mutateRelationshipDataBeforeSave($itemData, record: $record);
108
+
109
+                                        if ($itemData === null) {
110
+                                            continue;
111
+                                        }
112
+
113
+                                        $translatableContentDriver ?
114
+                                            $translatableContentDriver->updateRecord($record, $itemData) :
115
+                                            $record->fill($itemData)->save();
116
+
117
+                                        continue;
118
+                                    }
119
+
120
+                                    $relatedModel = $component->getRelatedModel();
121
+
122
+                                    $itemData = $component->mutateRelationshipDataBeforeCreate($itemData);
123
+
124
+                                    if ($itemData === null) {
125
+                                        continue;
126
+                                    }
127
+
128
+                                    if ($translatableContentDriver) {
129
+                                        $record = $translatableContentDriver->makeRecord($relatedModel, $itemData);
130
+                                    } else {
131
+                                        $record = new $relatedModel;
132
+                                        $record->fill($itemData);
133
+                                    }
134
+
135
+                                    $record = $relationship->save($record);
136
+                                    $item->model($record)->saveRelationships();
137
+                                    $existingRecords->push($record);
138
+                                }
139
+
140
+                                $component->getRecord()->setRelation($component->getRelationshipName(), $existingRecords);
141
+
142
+                                /** @var Bill $bill */
143
+                                $bill = $component->getRecord();
144
+
145
+                                // Recalculate totals for line items
146
+                                $bill->lineItems()->each(function (DocumentLineItem $lineItem) {
147
+                                    $lineItem->updateQuietly([
148
+                                        'tax_total' => $lineItem->calculateTaxTotal()->getAmount(),
149
+                                        'discount_total' => $lineItem->calculateDiscountTotal()->getAmount(),
150
+                                    ]);
151
+                                });
152
+
153
+                                $subtotal = $bill->lineItems()->sum('subtotal') / 100;
154
+                                $taxTotal = $bill->lineItems()->sum('tax_total') / 100;
155
+                                $discountTotal = $bill->lineItems()->sum('discount_total') / 100;
156
+                                $grandTotal = $subtotal + $taxTotal - $discountTotal;
157
+
158
+                                $bill->updateQuietly([
159
+                                    'subtotal' => $subtotal,
160
+                                    'tax_total' => $taxTotal,
161
+                                    'discount_total' => $discountTotal,
162
+                                    'total' => $grandTotal,
163
+                                ]);
164
+                            })
165
+                            ->headers([
166
+                                Header::make('Items')->width('15%'),
167
+                                Header::make('Description')->width('25%'),
168
+                                Header::make('Quantity')->width('10%'),
169
+                                Header::make('Price')->width('10%'),
170
+                                Header::make('Taxes')->width('15%'),
171
+                                Header::make('Discounts')->width('15%'),
172
+                                Header::make('Amount')->width('10%')->align('right'),
173
+                            ])
174
+                            ->schema([
175
+                                Forms\Components\Select::make('offering_id')
176
+                                    ->relationship('purchasableOffering', 'name')
177
+                                    ->preload()
178
+                                    ->searchable()
179
+                                    ->required()
180
+                                    ->live()
181
+                                    ->afterStateUpdated(function (Forms\Set $set, Forms\Get $get, $state) {
182
+                                        $offeringId = $state;
183
+                                        $offeringRecord = Offering::with('purchaseTaxes')->find($offeringId);
184
+
185
+                                        if ($offeringRecord) {
186
+                                            $set('description', $offeringRecord->description);
187
+                                            $set('unit_price', $offeringRecord->price);
188
+                                            $set('purchaseTaxes', $offeringRecord->purchaseTaxes->pluck('id')->toArray());
189
+                                            $set('purchaseDiscounts', $offeringRecord->purchaseDiscounts->pluck('id')->toArray());
190
+                                        }
191
+                                    }),
192
+                                Forms\Components\TextInput::make('description'),
193
+                                Forms\Components\TextInput::make('quantity')
194
+                                    ->required()
195
+                                    ->numeric()
196
+                                    ->live()
197
+                                    ->default(1),
198
+                                Forms\Components\TextInput::make('unit_price')
199
+                                    ->hiddenLabel()
200
+                                    ->numeric()
201
+                                    ->live()
202
+                                    ->default(0),
203
+                                Forms\Components\Select::make('purchaseTaxes')
204
+                                    ->relationship('purchaseTaxes', 'name')
205
+                                    ->preload()
206
+                                    ->multiple()
207
+                                    ->live()
208
+                                    ->searchable(),
209
+                                Forms\Components\Select::make('purchaseDiscounts')
210
+                                    ->relationship('purchaseDiscounts', 'name')
211
+                                    ->preload()
212
+                                    ->multiple()
213
+                                    ->live()
214
+                                    ->searchable(),
215
+                                Forms\Components\Placeholder::make('total')
216
+                                    ->hiddenLabel()
217
+                                    ->content(function (Forms\Get $get) {
218
+                                        $quantity = max((float) ($get('quantity') ?? 0), 0);
219
+                                        $unitPrice = max((float) ($get('unit_price') ?? 0), 0);
220
+                                        $purchaseTaxes = $get('purchaseTaxes') ?? [];
221
+                                        $purchaseDiscounts = $get('purchaseDiscounts') ?? [];
222
+
223
+                                        $subtotal = $quantity * $unitPrice;
224
+
225
+                                        // Calculate tax amount based on subtotal
226
+                                        $taxAmount = 0;
227
+                                        if (! empty($purchaseTaxes)) {
228
+                                            $taxRates = Adjustment::whereIn('id', $purchaseTaxes)->pluck('rate');
229
+                                            $taxAmount = collect($taxRates)->sum(fn ($rate) => $subtotal * ($rate / 100));
230
+                                        }
231
+
232
+                                        // Calculate discount amount based on subtotal
233
+                                        $discountAmount = 0;
234
+                                        if (! empty($purchaseDiscounts)) {
235
+                                            $discountRates = Adjustment::whereIn('id', $purchaseDiscounts)->pluck('rate');
236
+                                            $discountAmount = collect($discountRates)->sum(fn ($rate) => $subtotal * ($rate / 100));
237
+                                        }
238
+
239
+                                        // Final total
240
+                                        $total = $subtotal + ($taxAmount - $discountAmount);
241
+
242
+                                        return money($total, CurrencyAccessor::getDefaultCurrency(), true)->format();
243
+                                    }),
244
+                            ]),
245
+                        Forms\Components\Grid::make(6)
246
+                            ->schema([
247
+                                Forms\Components\ViewField::make('totals')
248
+                                    ->columnStart(5)
249
+                                    ->columnSpan(2)
250
+                                    ->view('filament.forms.components.bill-totals'),
251
+                            ]),
252
+                    ]),
22
             ]);
253
             ]);
23
     }
254
     }
24
 
255
 
26
     {
257
     {
27
         return $table
258
         return $table
28
             ->columns([
259
             ->columns([
29
-                //
260
+                Tables\Columns\TextColumn::make('status')
261
+                    ->badge()
262
+                    ->searchable(),
263
+                Tables\Columns\TextColumn::make('due_date')
264
+                    ->label('Due')
265
+                    ->formatStateUsing(function (Tables\Columns\TextColumn $column, mixed $state) {
266
+                        if (blank($state)) {
267
+                            return null;
268
+                        }
269
+
270
+                        $date = Carbon::parse($state)
271
+                            ->setTimezone($timezone ?? $column->getTimezone());
272
+
273
+                        if ($date->isToday()) {
274
+                            return 'Today';
275
+                        }
276
+
277
+                        return $date->diffForHumans([
278
+                            'options' => CarbonInterface::ONE_DAY_WORDS,
279
+                        ]);
280
+                    })
281
+                    ->sortable(),
282
+                Tables\Columns\TextColumn::make('date')
283
+                    ->date()
284
+                    ->sortable(),
285
+                Tables\Columns\TextColumn::make('bill_number')
286
+                    ->label('Number')
287
+                    ->searchable(),
288
+                Tables\Columns\TextColumn::make('vendor.name')
289
+                    ->sortable(),
290
+                Tables\Columns\TextColumn::make('total')
291
+                    ->currency(),
292
+                Tables\Columns\TextColumn::make('amount_paid')
293
+                    ->label('Amount Paid')
294
+                    ->currency(),
295
+                Tables\Columns\TextColumn::make('amount_due')
296
+                    ->label('Amount Due')
297
+                    ->currency(),
30
             ]);
298
             ]);
31
     }
299
     }
32
 
300
 

+ 9
- 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\RedirectToListPage;
5
 use App\Filament\Company\Resources\Purchases\BillResource;
6
 use App\Filament\Company\Resources\Purchases\BillResource;
6
 use Filament\Resources\Pages\CreateRecord;
7
 use Filament\Resources\Pages\CreateRecord;
8
+use Filament\Support\Enums\MaxWidth;
7
 
9
 
8
 class CreateBill extends CreateRecord
10
 class CreateBill extends CreateRecord
9
 {
11
 {
12
+    use RedirectToListPage;
13
+
10
     protected static string $resource = BillResource::class;
14
     protected static string $resource = BillResource::class;
15
+
16
+    public function getMaxContentWidth(): MaxWidth | string | null
17
+    {
18
+        return MaxWidth::Full;
19
+    }
11
 }
20
 }

+ 9
- 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\RedirectToListPage;
5
 use App\Filament\Company\Resources\Purchases\BillResource;
6
 use App\Filament\Company\Resources\Purchases\BillResource;
6
 use Filament\Actions;
7
 use Filament\Actions;
7
 use Filament\Resources\Pages\EditRecord;
8
 use Filament\Resources\Pages\EditRecord;
9
+use Filament\Support\Enums\MaxWidth;
8
 
10
 
9
 class EditBill extends EditRecord
11
 class EditBill extends EditRecord
10
 {
12
 {
13
+    use RedirectToListPage;
14
+
11
     protected static string $resource = BillResource::class;
15
     protected static string $resource = BillResource::class;
12
 
16
 
13
     protected function getHeaderActions(): array
17
     protected function getHeaderActions(): array
16
             Actions\DeleteAction::make(),
20
             Actions\DeleteAction::make(),
17
         ];
21
         ];
18
     }
22
     }
23
+
24
+    public function getMaxContentWidth(): MaxWidth | string | null
25
+    {
26
+        return MaxWidth::Full;
27
+    }
19
 }
28
 }

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

219
                             ])
219
                             ])
220
                             ->schema([
220
                             ->schema([
221
                                 Forms\Components\Select::make('offering_id')
221
                                 Forms\Components\Select::make('offering_id')
222
-                                    ->relationship('offering', 'name')
222
+                                    ->relationship('sellableOffering', 'name')
223
                                     ->preload()
223
                                     ->preload()
224
                                     ->searchable()
224
                                     ->searchable()
225
                                     ->required()
225
                                     ->required()
425
                     ->label('Number')
425
                     ->label('Number')
426
                     ->searchable(),
426
                     ->searchable(),
427
                 Tables\Columns\TextColumn::make('client.name')
427
                 Tables\Columns\TextColumn::make('client.name')
428
-                    ->numeric()
429
                     ->sortable(),
428
                     ->sortable(),
430
                 Tables\Columns\TextColumn::make('total')
429
                 Tables\Columns\TextColumn::make('total')
431
                     ->currency(),
430
                     ->currency(),

+ 33
- 0
app/Models/Accounting/Bill.php 查看文件

65
     {
65
     {
66
         return $this->morphMany(Payment::class, 'payable');
66
         return $this->morphMany(Payment::class, 'payable');
67
     }
67
     }
68
+
69
+    public static function getNextDocumentNumber(): string
70
+    {
71
+        $company = auth()->user()->currentCompany;
72
+
73
+        if (! $company) {
74
+            throw new \RuntimeException('No current company is set for the user.');
75
+        }
76
+
77
+        $defaultBillSettings = $company->defaultBill;
78
+
79
+        $numberPrefix = $defaultBillSettings->number_prefix;
80
+        $numberDigits = $defaultBillSettings->number_digits;
81
+
82
+        $latestDocument = static::query()
83
+            ->whereNotNull('bill_number')
84
+            ->latest('bill_number')
85
+            ->first();
86
+
87
+        $lastNumberNumericPart = $latestDocument
88
+            ? (int) substr($latestDocument->bill_number, strlen($numberPrefix))
89
+            : 0;
90
+
91
+        $numberNext = $lastNumberNumericPart + 1;
92
+
93
+        return $defaultBillSettings->getNumberNext(
94
+            padded: true,
95
+            format: true,
96
+            prefix: $numberPrefix,
97
+            digits: $numberDigits,
98
+            next: $numberNext
99
+        );
100
+    }
68
 }
101
 }

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

58
         return $this->belongsTo(Offering::class);
58
         return $this->belongsTo(Offering::class);
59
     }
59
     }
60
 
60
 
61
+    public function sellableOffering(): BelongsTo
62
+    {
63
+        return $this->offering()->where('sellable', true);
64
+    }
65
+
66
+    public function purchasableOffering(): BelongsTo
67
+    {
68
+        return $this->offering()->where('purchasable', true);
69
+    }
70
+
61
     public function adjustments(): MorphToMany
71
     public function adjustments(): MorphToMany
62
     {
72
     {
63
         return $this->morphToMany(Adjustment::class, 'adjustmentable', 'adjustmentables');
73
         return $this->morphToMany(Adjustment::class, 'adjustmentable', 'adjustmentables');
68
         return $this->adjustments()->where('category', AdjustmentCategory::Tax)->where('type', AdjustmentType::Sales);
78
         return $this->adjustments()->where('category', AdjustmentCategory::Tax)->where('type', AdjustmentType::Sales);
69
     }
79
     }
70
 
80
 
81
+    public function purchaseTaxes(): MorphToMany
82
+    {
83
+        return $this->adjustments()->where('category', AdjustmentCategory::Tax)->where('type', AdjustmentType::Purchase);
84
+    }
85
+
71
     public function salesDiscounts(): MorphToMany
86
     public function salesDiscounts(): MorphToMany
72
     {
87
     {
73
         return $this->adjustments()->where('category', AdjustmentCategory::Discount)->where('type', AdjustmentType::Sales);
88
         return $this->adjustments()->where('category', AdjustmentCategory::Discount)->where('type', AdjustmentType::Sales);
74
     }
89
     }
75
 
90
 
91
+    public function purchaseDiscounts(): MorphToMany
92
+    {
93
+        return $this->adjustments()->where('category', AdjustmentCategory::Discount)->where('type', AdjustmentType::Purchase);
94
+    }
95
+
76
     public function taxes(): MorphToMany
96
     public function taxes(): MorphToMany
77
     {
97
     {
78
         return $this->adjustments()->where('category', AdjustmentCategory::Tax);
98
         return $this->adjustments()->where('category', AdjustmentCategory::Tax);

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

90
             ->first();
90
             ->first();
91
 
91
 
92
         $lastNumberNumericPart = $latestDocument
92
         $lastNumberNumericPart = $latestDocument
93
-            ? (int) substr($latestDocument->document_number, strlen($numberPrefix))
93
+            ? (int) substr($latestDocument->invoice_number, strlen($numberPrefix))
94
             : 0;
94
             : 0;
95
 
95
 
96
         $numberNext = $lastNumberNumericPart + 1;
96
         $numberNext = $lastNumberNumericPart + 1;

+ 62
- 0
app/View/Models/BillTotalViewModel.php 查看文件

1
+<?php
2
+
3
+namespace App\View\Models;
4
+
5
+use App\Models\Accounting\Adjustment;
6
+use App\Models\Accounting\Bill;
7
+use App\Utilities\Currency\CurrencyConverter;
8
+
9
+class BillTotalViewModel
10
+{
11
+    public function __construct(
12
+        public ?Bill $bill,
13
+        public ?array $data = null
14
+    ) {}
15
+
16
+    public function buildViewData(): array
17
+    {
18
+        $lineItems = collect($this->data['lineItems'] ?? []);
19
+
20
+        $subtotal = $lineItems->sum(function ($item) {
21
+            $quantity = max((float) ($item['quantity'] ?? 0), 0);
22
+            $unitPrice = max((float) ($item['unit_price'] ?? 0), 0);
23
+
24
+            return $quantity * $unitPrice;
25
+        });
26
+
27
+        $taxTotal = $lineItems->reduce(function ($carry, $item) {
28
+            $quantity = max((float) ($item['quantity'] ?? 0), 0);
29
+            $unitPrice = max((float) ($item['unit_price'] ?? 0), 0);
30
+            $purchaseTaxes = $item['purchaseTaxes'] ?? [];
31
+            $lineTotal = $quantity * $unitPrice;
32
+
33
+            $taxAmount = Adjustment::whereIn('id', $purchaseTaxes)
34
+                ->pluck('rate')
35
+                ->sum(fn ($rate) => $lineTotal * ($rate / 100));
36
+
37
+            return $carry + $taxAmount;
38
+        }, 0);
39
+
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;
45
+
46
+            $discountAmount = Adjustment::whereIn('id', $purchaseDiscounts)
47
+                ->pluck('rate')
48
+                ->sum(fn ($rate) => $lineTotal * ($rate / 100));
49
+
50
+            return $carry + $discountAmount;
51
+        }, 0);
52
+
53
+        $grandTotal = $subtotal + ($taxTotal - $discountTotal);
54
+
55
+        return [
56
+            'subtotal' => CurrencyConverter::formatToMoney($subtotal),
57
+            'taxTotal' => CurrencyConverter::formatToMoney($taxTotal),
58
+            'discountTotal' => CurrencyConverter::formatToMoney($discountTotal),
59
+            'grandTotal' => CurrencyConverter::formatToMoney($grandTotal),
60
+        ];
61
+    }
62
+}

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

19
             $table->string('order_number')->nullable(); // PO, SO, etc.
19
             $table->string('order_number')->nullable(); // PO, SO, etc.
20
             $table->date('date')->nullable();
20
             $table->date('date')->nullable();
21
             $table->date('due_date')->nullable();
21
             $table->date('due_date')->nullable();
22
-            $table->string('status')->default('draft');
22
+            $table->string('status')->default('unpaid');
23
             $table->string('currency_code')->nullable();
23
             $table->string('currency_code')->nullable();
24
             $table->integer('subtotal')->default(0);
24
             $table->integer('subtotal')->default(0);
25
             $table->integer('tax_total')->default(0);
25
             $table->integer('tax_total')->default(0);

+ 2
- 1
resources/data/lang/en.json 查看文件

211
     "Invoice Header": "Invoice Header",
211
     "Invoice Header": "Invoice Header",
212
     "Invoice Details": "Invoice Details",
212
     "Invoice Details": "Invoice Details",
213
     "Footer": "Footer",
213
     "Footer": "Footer",
214
-    "Invoice Footer": "Invoice Footer"
214
+    "Invoice Footer": "Invoice Footer",
215
+    "Bill Details": "Bill Details"
215
 }
216
 }

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

1
+@use('App\Utilities\Currency\CurrencyAccessor')
2
+
3
+@php
4
+    $data = $this->form->getRawState();
5
+    $viewModel = new \App\View\Models\BillTotalViewModel($this->record, $data);
6
+    extract($viewModel->buildViewData(), \EXTR_SKIP);
7
+@endphp
8
+
9
+<div class="totals-summary w-full pr-14">
10
+    <table class="w-full text-right table-fixed">
11
+        <tbody>
12
+            <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>
19
+            </tr>
20
+            <tr>
21
+                <td class="w-2/3 text-sm px-4 py-2 font-medium leading-6 text-gray-950 dark:text-white">Discounts:</td>
22
+                <td class="w-1/3 text-sm pl-4 py-2 leading-6">({{ $discountTotal }})</td>
23
+            </tr>
24
+            <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>
27
+            </tr>
28
+        </tbody>
29
+    </table>
30
+</div>
31
+
32
+
33
+
34
+

Loading…
取消
儲存