Andrew Wallo пре 10 месеци
родитељ
комит
b950fff024
46 измењених фајлова са 1279 додато и 802 уклоњено
  1. 28
    0
      app/Enums/Accounting/BillStatus.php
  2. 36
    0
      app/Enums/Accounting/InvoiceStatus.php
  3. 20
    0
      app/Filament/Company/Pages/Reports.php
  4. 0
    465
      app/Filament/Company/Resources/Accounting/DocumentResource.php
  5. 0
    17
      app/Filament/Company/Resources/Accounting/DocumentResource/Pages/CreateDocument.php
  6. 0
    27
      app/Filament/Company/Resources/Accounting/DocumentResource/Pages/EditDocument.php
  7. 0
    19
      app/Filament/Company/Resources/Accounting/DocumentResource/Pages/ListDocuments.php
  8. 1
    1
      app/Filament/Company/Resources/Common/ClientResource.php
  9. 3
    0
      app/Filament/Company/Resources/Common/ClientResource/Pages/CreateClient.php
  10. 3
    0
      app/Filament/Company/Resources/Common/ClientResource/Pages/EditClient.php
  11. 1
    1
      app/Filament/Company/Resources/Common/VendorResource.php
  12. 3
    0
      app/Filament/Company/Resources/Common/VendorResource/Pages/CreateVendor.php
  13. 3
    0
      app/Filament/Company/Resources/Common/VendorResource/Pages/EditVendor.php
  14. 48
    0
      app/Filament/Company/Resources/Purchases/BillResource.php
  15. 11
    0
      app/Filament/Company/Resources/Purchases/BillResource/Pages/CreateBill.php
  16. 19
    0
      app/Filament/Company/Resources/Purchases/BillResource/Pages/EditBill.php
  17. 19
    0
      app/Filament/Company/Resources/Purchases/BillResource/Pages/ListBills.php
  18. 428
    16
      app/Filament/Company/Resources/Sales/InvoiceResource.php
  19. 3
    7
      app/Filament/Company/Resources/Sales/InvoiceResource/Pages/CreateInvoice.php
  20. 3
    0
      app/Filament/Company/Resources/Sales/InvoiceResource/Pages/EditInvoice.php
  21. 68
    0
      app/Models/Accounting/Bill.php
  22. 3
    3
      app/Models/Accounting/DocumentLineItem.php
  23. 13
    27
      app/Models/Accounting/Invoice.php
  24. 3
    4
      app/Models/Banking/Payment.php
  25. 2
    4
      app/Models/Common/Client.php
  26. 16
    0
      app/Models/Common/Contact.php
  27. 2
    4
      app/Models/Common/Vendor.php
  28. 12
    2
      app/Models/Company.php
  29. 0
    68
      app/Observers/DocumentObserver.php
  30. 5
    3
      app/Providers/FilamentCompaniesServiceProvider.php
  31. 1
    1
      app/Providers/MacroServiceProvider.php
  32. 16
    16
      app/View/Models/InvoiceTotalViewModel.php
  33. 97
    97
      composer.lock
  34. 2
    2
      database/factories/Accounting/BillFactory.php
  35. 23
    0
      database/factories/Accounting/InvoiceFactory.php
  36. 42
    2
      database/factories/Common/AddressFactory.php
  37. 34
    2
      database/factories/Common/ClientFactory.php
  38. 66
    2
      database/factories/Common/ContactFactory.php
  39. 94
    2
      database/factories/Common/OfferingFactory.php
  40. 69
    2
      database/factories/Common/VendorFactory.php
  41. 30
    0
      database/factories/CompanyFactory.php
  42. 1
    1
      database/migrations/2024_11_13_215818_create_payments_table.php
  43. 1
    1
      database/migrations/2024_11_13_220301_create_document_line_items_table.php
  44. 43
    0
      database/migrations/2024_11_27_221657_create_bills_table.php
  45. 3
    5
      database/migrations/2024_11_27_223015_create_invoices_table.php
  46. 4
    1
      database/seeders/DatabaseSeeder.php

+ 28
- 0
app/Enums/Accounting/BillStatus.php Прегледај датотеку

@@ -0,0 +1,28 @@
1
+<?php
2
+
3
+namespace App\Enums\Accounting;
4
+
5
+use Filament\Support\Contracts\HasColor;
6
+use Filament\Support\Contracts\HasLabel;
7
+
8
+enum BillStatus: string implements HasColor, HasLabel
9
+{
10
+    case Partial = 'partial';
11
+    case Paid = 'paid';
12
+    case Unpaid = 'unpaid';
13
+    case Void = 'void';
14
+
15
+    public function getLabel(): ?string
16
+    {
17
+        return $this->name;
18
+    }
19
+
20
+    public function getColor(): string | array | null
21
+    {
22
+        return match ($this) {
23
+            self::Partial, self::Unpaid => 'warning',
24
+            self::Paid => 'success',
25
+            self::Void => 'gray',
26
+        };
27
+    }
28
+}

+ 36
- 0
app/Enums/Accounting/InvoiceStatus.php Прегледај датотеку

@@ -0,0 +1,36 @@
1
+<?php
2
+
3
+namespace App\Enums\Accounting;
4
+
5
+use Filament\Support\Contracts\HasColor;
6
+use Filament\Support\Contracts\HasLabel;
7
+
8
+enum InvoiceStatus: 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
+    public function getLabel(): ?string
22
+    {
23
+        return $this->name;
24
+    }
25
+
26
+    public function getColor(): string | array | null
27
+    {
28
+        return match ($this) {
29
+            self::Draft, self::Void => 'gray',
30
+            self::Sent => 'primary',
31
+            self::Partial => 'warning',
32
+            self::Paid => 'success',
33
+            self::Overdue => 'danger',
34
+        };
35
+    }
36
+}

+ 20
- 0
app/Filament/Company/Pages/Reports.php Прегледај датотеку

@@ -11,6 +11,7 @@ use App\Filament\Company\Pages\Reports\TrialBalance;
11 11
 use App\Infolists\Components\ReportEntry;
12 12
 use Filament\Infolists\Components\Section;
13 13
 use Filament\Infolists\Infolist;
14
+use Filament\Navigation\NavigationItem;
14 15
 use Filament\Pages\Page;
15 16
 use Filament\Support\Colors\Color;
16 17
 
@@ -20,6 +21,25 @@ class Reports extends Page
20 21
 
21 22
     protected static string $view = 'filament.company.pages.reports';
22 23
 
24
+    public static function getNavigationItems(): array
25
+    {
26
+        return [
27
+            NavigationItem::make(static::getNavigationLabel())
28
+                ->group(static::getNavigationGroup())
29
+                ->parentItem(static::getNavigationParentItem())
30
+                ->icon(static::getNavigationIcon())
31
+                ->activeIcon(static::getActiveNavigationIcon())
32
+                ->isActiveWhen(fn (): bool => request()->routeIs([
33
+                    static::getRouteName(),
34
+                    static::getRouteName() . '.*',
35
+                ]))
36
+                ->sort(static::getNavigationSort())
37
+                ->badge(static::getNavigationBadge(), color: static::getNavigationBadgeColor())
38
+                ->badgeTooltip(static::getNavigationBadgeTooltip())
39
+                ->url(static::getNavigationUrl()),
40
+        ];
41
+    }
42
+
23 43
     public function reportsInfolist(Infolist $infolist): Infolist
24 44
     {
25 45
         return $infolist

+ 0
- 465
app/Filament/Company/Resources/Accounting/DocumentResource.php Прегледај датотеку

@@ -1,465 +0,0 @@
1
-<?php
2
-
3
-namespace App\Filament\Company\Resources\Accounting;
4
-
5
-use App\Enums\Accounting\AdjustmentCategory;
6
-use App\Filament\Company\Resources\Accounting\DocumentResource\Pages;
7
-use App\Models\Accounting\Adjustment;
8
-use App\Models\Accounting\Document;
9
-use App\Models\Accounting\DocumentLineItem;
10
-use App\Models\Common\Offering;
11
-use App\Utilities\Currency\CurrencyAccessor;
12
-use Awcodes\TableRepeater\Components\TableRepeater;
13
-use Awcodes\TableRepeater\Header;
14
-use Carbon\CarbonInterface;
15
-use Filament\Forms;
16
-use Filament\Forms\Components\FileUpload;
17
-use Filament\Forms\Form;
18
-use Filament\Resources\Resource;
19
-use Filament\Support\Enums\MaxWidth;
20
-use Filament\Tables;
21
-use Filament\Tables\Table;
22
-use Illuminate\Database\Eloquent\Model;
23
-use Illuminate\Support\Carbon;
24
-use Illuminate\Support\Facades\Auth;
25
-use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
26
-
27
-class DocumentResource extends Resource
28
-{
29
-    protected static ?string $model = Document::class;
30
-
31
-    protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
32
-
33
-    public static function form(Form $form): Form
34
-    {
35
-        $company = Auth::user()->currentCompany;
36
-
37
-        return $form
38
-            ->schema([
39
-                Forms\Components\Section::make('Invoice Header')
40
-                    ->collapsible()
41
-                    ->schema([
42
-                        Forms\Components\Split::make([
43
-                            Forms\Components\Group::make([
44
-                                FileUpload::make('logo')
45
-                                    ->openable()
46
-                                    ->maxSize(1024)
47
-                                    ->localizeLabel()
48
-                                    ->visibility('public')
49
-                                    ->disk('public')
50
-                                    ->directory('logos/document')
51
-                                    ->imageResizeMode('contain')
52
-                                    ->imageCropAspectRatio('3:2')
53
-                                    ->panelAspectRatio('3:2')
54
-                                    ->maxWidth(MaxWidth::ExtraSmall)
55
-                                    ->panelLayout('integrated')
56
-                                    ->removeUploadedFileButtonPosition('center bottom')
57
-                                    ->uploadButtonPosition('center bottom')
58
-                                    ->uploadProgressIndicatorPosition('center bottom')
59
-                                    ->getUploadedFileNameForStorageUsing(
60
-                                        static fn (TemporaryUploadedFile $file): string => (string) str($file->getClientOriginalName())
61
-                                            ->prepend(Auth::user()->currentCompany->id . '_'),
62
-                                    )
63
-                                    ->acceptedFileTypes(['image/png', 'image/jpeg', 'image/gif']),
64
-                            ]),
65
-                            Forms\Components\Group::make([
66
-                                Forms\Components\TextInput::make('header')
67
-                                    ->default(fn () => $company->defaultInvoice->header),
68
-                                Forms\Components\TextInput::make('subheader')
69
-                                    ->default(fn () => $company->defaultInvoice->subheader),
70
-                                Forms\Components\View::make('filament.forms.components.company-info')
71
-                                    ->viewData([
72
-                                        'company_name' => $company->name,
73
-                                        'company_address' => $company->profile->address,
74
-                                        'company_city' => $company->profile->city?->name,
75
-                                        'company_state' => $company->profile->state?->name,
76
-                                        'company_zip' => $company->profile->zip_code,
77
-                                        'company_country' => $company->profile->state?->country->name,
78
-                                    ]),
79
-                            ])->grow(true),
80
-                        ])->from('md'),
81
-                    ]),
82
-                Forms\Components\Section::make('Invoice Details')
83
-                    ->schema([
84
-                        Forms\Components\Split::make([
85
-                            Forms\Components\Group::make([
86
-                                Forms\Components\Select::make('client_id')
87
-                                    ->relationship('client', 'name')
88
-                                    ->preload()
89
-                                    ->searchable()
90
-                                    ->required(),
91
-                            ]),
92
-                            Forms\Components\Group::make([
93
-                                Forms\Components\TextInput::make('document_number')
94
-                                    ->label('Invoice Number')
95
-                                    ->default(fn () => Document::getNextDocumentNumber()),
96
-                                Forms\Components\TextInput::make('order_number')
97
-                                    ->label('P.O/S.O Number'),
98
-                                Forms\Components\DatePicker::make('date')
99
-                                    ->label('Invoice Date')
100
-                                    ->default(now()),
101
-                                Forms\Components\DatePicker::make('due_date')
102
-                                    ->label('Payment Due')
103
-                                    ->default(function () use ($company) {
104
-                                        return now()->addDays($company->defaultInvoice->payment_terms->getDays());
105
-                                    }),
106
-                            ])->grow(true),
107
-                        ])->from('md'),
108
-                        TableRepeater::make('lineItems')
109
-                            ->relationship()
110
-                            ->saveRelationshipsUsing(function (TableRepeater $component, Forms\Contracts\HasForms $livewire, ?array $state) {
111
-                                if (! is_array($state)) {
112
-                                    $state = [];
113
-                                }
114
-
115
-                                $relationship = $component->getRelationship();
116
-
117
-                                $existingRecords = $component->getCachedExistingRecords();
118
-
119
-                                $recordsToDelete = [];
120
-
121
-                                foreach ($existingRecords->pluck($relationship->getRelated()->getKeyName()) as $keyToCheckForDeletion) {
122
-                                    if (array_key_exists("record-{$keyToCheckForDeletion}", $state)) {
123
-                                        continue;
124
-                                    }
125
-
126
-                                    $recordsToDelete[] = $keyToCheckForDeletion;
127
-                                    $existingRecords->forget("record-{$keyToCheckForDeletion}");
128
-                                }
129
-
130
-                                $relationship
131
-                                    ->whereKey($recordsToDelete)
132
-                                    ->get()
133
-                                    ->each(static fn (Model $record) => $record->delete());
134
-
135
-                                $childComponentContainers = $component->getChildComponentContainers(
136
-                                    withHidden: $component->shouldSaveRelationshipsWhenHidden(),
137
-                                );
138
-
139
-                                $itemOrder = 1;
140
-                                $orderColumn = $component->getOrderColumn();
141
-
142
-                                $translatableContentDriver = $livewire->makeFilamentTranslatableContentDriver();
143
-
144
-                                foreach ($childComponentContainers as $itemKey => $item) {
145
-                                    $itemData = $item->getState(shouldCallHooksBefore: false);
146
-
147
-                                    if ($orderColumn) {
148
-                                        $itemData[$orderColumn] = $itemOrder;
149
-
150
-                                        $itemOrder++;
151
-                                    }
152
-
153
-                                    if ($record = ($existingRecords[$itemKey] ?? null)) {
154
-                                        $itemData = $component->mutateRelationshipDataBeforeSave($itemData, record: $record);
155
-
156
-                                        if ($itemData === null) {
157
-                                            continue;
158
-                                        }
159
-
160
-                                        $translatableContentDriver ?
161
-                                            $translatableContentDriver->updateRecord($record, $itemData) :
162
-                                            $record->fill($itemData)->save();
163
-
164
-                                        continue;
165
-                                    }
166
-
167
-                                    $relatedModel = $component->getRelatedModel();
168
-
169
-                                    $itemData = $component->mutateRelationshipDataBeforeCreate($itemData);
170
-
171
-                                    if ($itemData === null) {
172
-                                        continue;
173
-                                    }
174
-
175
-                                    if ($translatableContentDriver) {
176
-                                        $record = $translatableContentDriver->makeRecord($relatedModel, $itemData);
177
-                                    } else {
178
-                                        $record = new $relatedModel;
179
-                                        $record->fill($itemData);
180
-                                    }
181
-
182
-                                    $record = $relationship->save($record);
183
-                                    $item->model($record)->saveRelationships();
184
-                                    $existingRecords->push($record);
185
-                                }
186
-
187
-                                $component->getRecord()->setRelation($component->getRelationshipName(), $existingRecords);
188
-
189
-                                /** @var Document $document */
190
-                                $document = $component->getRecord();
191
-
192
-                                // Recalculate totals for line items
193
-                                $document->lineItems()->each(function (DocumentLineItem $lineItem) {
194
-                                    $lineItem->updateQuietly([
195
-                                        'tax_total' => $lineItem->calculateTaxTotal()->getAmount(),
196
-                                        'discount_total' => $lineItem->calculateDiscountTotal()->getAmount(),
197
-                                    ]);
198
-                                });
199
-
200
-                                $subtotal = $document->lineItems()->sum('subtotal') / 100;
201
-                                $taxTotal = $document->lineItems()->sum('tax_total') / 100;
202
-                                $discountTotal = $document->lineItems()->sum('discount_total') / 100;
203
-                                $grandTotal = $subtotal + $taxTotal - $discountTotal;
204
-
205
-                                $document->updateQuietly([
206
-                                    'subtotal' => $subtotal,
207
-                                    'tax_total' => $taxTotal,
208
-                                    'discount_total' => $discountTotal,
209
-                                    'total' => $grandTotal,
210
-                                ]);
211
-                            })
212
-                            ->headers([
213
-                                Header::make('Items')->width('15%'),
214
-                                Header::make('Description')->width('25%'),
215
-                                Header::make('Quantity')->width('10%'),
216
-                                Header::make('Price')->width('10%'),
217
-                                Header::make('Taxes')->width('15%'),
218
-                                Header::make('Discounts')->width('15%'),
219
-                                Header::make('Amount')->width('10%')->align('right'),
220
-                            ])
221
-                            ->live()
222
-                            ->schema([
223
-                                Forms\Components\Select::make('offering_id')
224
-                                    ->relationship('offering', 'name')
225
-                                    ->preload()
226
-                                    ->searchable()
227
-                                    ->required()
228
-                                    ->afterStateUpdated(function (Forms\Set $set, Forms\Get $get, $state) {
229
-                                        $offeringId = $state;
230
-                                        $offeringRecord = Offering::with('salesTaxes')->find($offeringId);
231
-
232
-                                        if ($offeringRecord) {
233
-                                            $set('description', $offeringRecord->description);
234
-                                            $set('unit_price', $offeringRecord->price);
235
-                                            $set('salesTaxes', $offeringRecord->salesTaxes->pluck('id')->toArray());
236
-                                            $set('salesDiscounts', $offeringRecord->salesDiscounts->pluck('id')->toArray());
237
-                                        }
238
-                                    }),
239
-                                Forms\Components\TextInput::make('description'),
240
-                                Forms\Components\TextInput::make('quantity')
241
-                                    ->required()
242
-                                    ->numeric()
243
-                                    ->default(1),
244
-                                Forms\Components\TextInput::make('unit_price')
245
-                                    ->hiddenLabel()
246
-                                    ->numeric()
247
-                                    ->default(0),
248
-                                Forms\Components\Select::make('salesTaxes')
249
-                                    ->relationship('salesTaxes', 'name')
250
-                                    ->preload()
251
-                                    ->multiple()
252
-                                    ->searchable(),
253
-                                Forms\Components\Select::make('salesDiscounts')
254
-                                    ->relationship('salesDiscounts', 'name')
255
-                                    ->preload()
256
-                                    ->multiple()
257
-                                    ->searchable(),
258
-                                Forms\Components\Placeholder::make('total')
259
-                                    ->hiddenLabel()
260
-                                    ->content(function (Forms\Get $get) {
261
-                                        $quantity = $get('quantity') ?? 0;
262
-                                        $unitPrice = $get('unit_price') ?? 0;
263
-                                        $salesTaxes = $get('salesTaxes') ?? [];
264
-                                        $salesDiscounts = $get('salesDiscounts') ?? [];
265
-
266
-                                        // Base total (subtotal)
267
-                                        $subtotal = $quantity * $unitPrice;
268
-
269
-                                        // Calculate tax amount based on subtotal
270
-                                        $taxAmount = 0;
271
-                                        if (! empty($salesTaxes)) {
272
-                                            $taxRates = Adjustment::whereIn('id', $salesTaxes)->pluck('rate');
273
-                                            $taxAmount = collect($taxRates)->sum(fn ($rate) => $subtotal * ($rate / 100));
274
-                                        }
275
-
276
-                                        // Calculate discount amount based on subtotal
277
-                                        $discountAmount = 0;
278
-                                        if (! empty($salesDiscounts)) {
279
-                                            $discountRates = Adjustment::whereIn('id', $salesDiscounts)->pluck('rate');
280
-                                            $discountAmount = collect($discountRates)->sum(fn ($rate) => $subtotal * ($rate / 100));
281
-                                        }
282
-
283
-                                        // Final total
284
-                                        $total = $subtotal + ($taxAmount - $discountAmount);
285
-
286
-                                        return money($total, CurrencyAccessor::getDefaultCurrency(), true)->format();
287
-                                    }),
288
-                            ]),
289
-                        Forms\Components\Grid::make(6)
290
-                            ->schema([
291
-                                Forms\Components\ViewField::make('totals')
292
-                                    ->columnStart(5)
293
-                                    ->columnSpan(2)
294
-                                    ->view('filament.forms.components.invoice-totals'),
295
-                            ]),
296
-                        //                        Forms\Components\Repeater::make('lineItems')
297
-                        //                            ->relationship()
298
-                        //                            ->columns(8)
299
-                        //                            ->schema([
300
-                        //                                Forms\Components\Select::make('offering_id')
301
-                        //                                    ->relationship('offering', 'name')
302
-                        //                                    ->preload()
303
-                        //                                    ->columnSpan(2)
304
-                        //                                    ->searchable()
305
-                        //                                    ->required()
306
-                        //                                    ->live()
307
-                        //                                    ->afterStateUpdated(function (Forms\Set $set, $state) {
308
-                        //                                        $offeringId = $state;
309
-                        //                                        $offeringRecord = Offering::with('salesTaxes')->find($offeringId);
310
-                        //
311
-                        //                                        if ($offeringRecord) {
312
-                        //                                            $set('description', $offeringRecord->description);
313
-                        //                                            $set('unit_price', $offeringRecord->price);
314
-                        //                                            $set('total', $offeringRecord->price);
315
-                        //
316
-                        //                                            $salesTaxes = $offeringRecord->salesTaxes->map(function ($tax) {
317
-                        //                                                return [
318
-                        //                                                    'id' => $tax->id,
319
-                        //                                                    'amount' => null, // Amount will be calculated dynamically
320
-                        //                                                ];
321
-                        //                                            })->toArray();
322
-                        //
323
-                        //                                            $set('taxes', $salesTaxes);
324
-                        //                                        }
325
-                        //                                    }),
326
-                        //                                Forms\Components\TextInput::make('description')
327
-                        //                                    ->columnSpan(3)
328
-                        //                                    ->required(),
329
-                        //                                Forms\Components\TextInput::make('quantity')
330
-                        //                                    ->required()
331
-                        //                                    ->numeric()
332
-                        //                                    ->live()
333
-                        //                                    ->default(1),
334
-                        //                                Forms\Components\TextInput::make('unit_price')
335
-                        //                                    ->live()
336
-                        //                                    ->numeric()
337
-                        //                                    ->default(0),
338
-                        //                                Forms\Components\Placeholder::make('total')
339
-                        //                                    ->content(function (Forms\Get $get) {
340
-                        //                                        $quantity = $get('quantity');
341
-                        //                                        $unitPrice = $get('unit_price');
342
-                        //
343
-                        //                                        if ($quantity && $unitPrice) {
344
-                        //                                            return $quantity * $unitPrice;
345
-                        //                                        }
346
-                        //                                    }),
347
-                        //                                TableRepeater::make('taxes')
348
-                        //                                    ->relationship()
349
-                        //                                    ->columnSpanFull()
350
-                        //                                    ->columnStart(6)
351
-                        //                                    ->headers([
352
-                        //                                        Header::make('')->width('200px'),
353
-                        //                                        Header::make('')->width('50px')->align('right'),
354
-                        //                                    ])
355
-                        //                                    ->defaultItems(0)
356
-                        //                                    ->schema([
357
-                        //                                        Forms\Components\Select::make('id') // The ID of the adjustment being attached.
358
-                        //                                            ->label('Tax Adjustment')
359
-                        //                                            ->options(
360
-                        //                                                Adjustment::query()
361
-                        //                                                    ->where('category', AdjustmentCategory::Tax)
362
-                        //                                                    ->pluck('name', 'id')
363
-                        //                                            )
364
-                        //                                            ->preload()
365
-                        //                                            ->searchable()
366
-                        //                                            ->required()
367
-                        //                                            ->live(),
368
-                        //                                        Forms\Components\Placeholder::make('amount')
369
-                        //                                            ->hiddenLabel()
370
-                        //                                            ->content(function (Forms\Get $get) {
371
-                        //                                                $quantity = $get('../../quantity') ?? 0; // Get parent quantity
372
-                        //                                                $unitPrice = $get('../../unit_price') ?? 0; // Get parent unit price
373
-                        //                                                $rate = Adjustment::find($get('id'))->rate ?? 0;
374
-                        //
375
-                        //                                                $total = $quantity * $unitPrice;
376
-                        //
377
-                        //                                                return $total * ($rate / 100);
378
-                        //                                            }),
379
-                        //                                    ]),
380
-                        //                            ]),
381
-                        Forms\Components\Textarea::make('terms')
382
-                            ->columnSpanFull(),
383
-                    ]),
384
-                Forms\Components\Section::make('Invoice Footer')
385
-                    ->collapsible()
386
-                    ->schema([
387
-                        Forms\Components\Textarea::make('footer')
388
-                            ->columnSpanFull(),
389
-                    ]),
390
-            ]);
391
-    }
392
-
393
-    public static function table(Table $table): Table
394
-    {
395
-        return $table
396
-            ->columns([
397
-                Tables\Columns\TextColumn::make('status')
398
-                    ->badge()
399
-                    ->searchable(),
400
-                Tables\Columns\TextColumn::make('due_date')
401
-                    ->label('Due')
402
-                    ->formatStateUsing(function (Tables\Columns\TextColumn $column, mixed $state) {
403
-                        if (blank($state)) {
404
-                            return null;
405
-                        }
406
-
407
-                        $date = Carbon::parse($state)
408
-                            ->setTimezone($timezone ?? $column->getTimezone());
409
-
410
-                        if ($date->isToday()) {
411
-                            return 'Today';
412
-                        }
413
-
414
-                        return $date->diffForHumans([
415
-                            'options' => CarbonInterface::ONE_DAY_WORDS,
416
-                        ]);
417
-                    })
418
-                    ->sortable(),
419
-                Tables\Columns\TextColumn::make('date')
420
-                    ->date()
421
-                    ->sortable(),
422
-                Tables\Columns\TextColumn::make('document_number')
423
-                    ->label('Number')
424
-                    ->searchable(),
425
-                Tables\Columns\TextColumn::make('client.name')
426
-                    ->numeric()
427
-                    ->sortable(),
428
-                Tables\Columns\TextColumn::make('total')
429
-                    ->currency(),
430
-                Tables\Columns\TextColumn::make('amount_paid')
431
-                    ->label('Amount Paid')
432
-                    ->currency(),
433
-                Tables\Columns\TextColumn::make('amount_due')
434
-                    ->label('Amount Due')
435
-                    ->currency(),
436
-            ])
437
-            ->filters([
438
-                //
439
-            ])
440
-            ->actions([
441
-                Tables\Actions\EditAction::make(),
442
-            ])
443
-            ->bulkActions([
444
-                Tables\Actions\BulkActionGroup::make([
445
-                    Tables\Actions\DeleteBulkAction::make(),
446
-                ]),
447
-            ]);
448
-    }
449
-
450
-    public static function getRelations(): array
451
-    {
452
-        return [
453
-            //
454
-        ];
455
-    }
456
-
457
-    public static function getPages(): array
458
-    {
459
-        return [
460
-            'index' => Pages\ListDocuments::route('/'),
461
-            'create' => Pages\CreateDocument::route('/create'),
462
-            'edit' => Pages\EditDocument::route('/{record}/edit'),
463
-        ];
464
-    }
465
-}

+ 0
- 17
app/Filament/Company/Resources/Accounting/DocumentResource/Pages/CreateDocument.php Прегледај датотеку

@@ -1,17 +0,0 @@
1
-<?php
2
-
3
-namespace App\Filament\Company\Resources\Accounting\DocumentResource\Pages;
4
-
5
-use App\Filament\Company\Resources\Accounting\DocumentResource;
6
-use Filament\Resources\Pages\CreateRecord;
7
-use Filament\Support\Enums\MaxWidth;
8
-
9
-class CreateDocument extends CreateRecord
10
-{
11
-    protected static string $resource = DocumentResource::class;
12
-
13
-    public function getMaxContentWidth(): MaxWidth | string | null
14
-    {
15
-        return MaxWidth::Full;
16
-    }
17
-}

+ 0
- 27
app/Filament/Company/Resources/Accounting/DocumentResource/Pages/EditDocument.php Прегледај датотеку

@@ -1,27 +0,0 @@
1
-<?php
2
-
3
-namespace App\Filament\Company\Resources\Accounting\DocumentResource\Pages;
4
-
5
-use App\Filament\Company\Resources\Accounting\DocumentResource;
6
-use Filament\Actions;
7
-use Filament\Resources\Pages\EditRecord;
8
-use Illuminate\Database\Eloquent\Model;
9
-
10
-class EditDocument extends EditRecord
11
-{
12
-    protected static string $resource = DocumentResource::class;
13
-
14
-    protected function getHeaderActions(): array
15
-    {
16
-        return [
17
-            Actions\DeleteAction::make(),
18
-        ];
19
-    }
20
-
21
-    protected function handleRecordUpdate(Model $record, array $data): Model
22
-    {
23
-        ray($data);
24
-
25
-        return parent::handleRecordUpdate($record, $data); // TODO: Change the autogenerated stub
26
-    }
27
-}

+ 0
- 19
app/Filament/Company/Resources/Accounting/DocumentResource/Pages/ListDocuments.php Прегледај датотеку

@@ -1,19 +0,0 @@
1
-<?php
2
-
3
-namespace App\Filament\Company\Resources\Accounting\DocumentResource\Pages;
4
-
5
-use App\Filament\Company\Resources\Accounting\DocumentResource;
6
-use Filament\Actions;
7
-use Filament\Resources\Pages\ListRecords;
8
-
9
-class ListDocuments extends ListRecords
10
-{
11
-    protected static string $resource = DocumentResource::class;
12
-
13
-    protected function getHeaderActions(): array
14
-    {
15
-        return [
16
-            Actions\CreateAction::make(),
17
-        ];
18
-    }
19
-}

+ 1
- 1
app/Filament/Company/Resources/Common/ClientResource.php Прегледај датотеку

@@ -261,7 +261,7 @@ class ClientResource extends Resource
261 261
                     ->searchable(),
262 262
                 Tables\Columns\TextColumn::make('primaryContact.phones')
263 263
                     ->label('Phone')
264
-                    ->state(fn (Client $client) => $client->primaryContact->primary_phone),
264
+                    ->state(fn (Client $client) => $client->primaryContact->first_available_phone),
265 265
             ])
266 266
             ->filters([
267 267
                 //

+ 3
- 0
app/Filament/Company/Resources/Common/ClientResource/Pages/CreateClient.php Прегледај датотеку

@@ -2,12 +2,15 @@
2 2
 
3 3
 namespace App\Filament\Company\Resources\Common\ClientResource\Pages;
4 4
 
5
+use App\Concerns\RedirectToListPage;
5 6
 use App\Filament\Company\Resources\Common\ClientResource;
6 7
 use Filament\Resources\Pages\CreateRecord;
7 8
 use Filament\Support\Enums\MaxWidth;
8 9
 
9 10
 class CreateClient extends CreateRecord
10 11
 {
12
+    use RedirectToListPage;
13
+
11 14
     protected static string $resource = ClientResource::class;
12 15
 
13 16
     public function getMaxContentWidth(): MaxWidth | string | null

+ 3
- 0
app/Filament/Company/Resources/Common/ClientResource/Pages/EditClient.php Прегледај датотеку

@@ -2,6 +2,7 @@
2 2
 
3 3
 namespace App\Filament\Company\Resources\Common\ClientResource\Pages;
4 4
 
5
+use App\Concerns\RedirectToListPage;
5 6
 use App\Filament\Company\Resources\Common\ClientResource;
6 7
 use Filament\Actions;
7 8
 use Filament\Resources\Pages\EditRecord;
@@ -9,6 +10,8 @@ use Filament\Support\Enums\MaxWidth;
9 10
 
10 11
 class EditClient extends EditRecord
11 12
 {
13
+    use RedirectToListPage;
14
+
12 15
     protected static string $resource = ClientResource::class;
13 16
 
14 17
     protected function getHeaderActions(): array

+ 1
- 1
app/Filament/Company/Resources/Common/VendorResource.php Прегледај датотеку

@@ -187,7 +187,7 @@ class VendorResource extends Resource
187 187
                     ->searchable(),
188 188
                 Tables\Columns\TextColumn::make('primaryContact.phones')
189 189
                     ->label('Phone')
190
-                    ->state(fn (Vendor $vendor) => $vendor->contact?->primary_phone),
190
+                    ->state(fn (Vendor $vendor) => $vendor->contact?->first_available_phone),
191 191
             ])
192 192
             ->filters([
193 193
                 //

+ 3
- 0
app/Filament/Company/Resources/Common/VendorResource/Pages/CreateVendor.php Прегледај датотеку

@@ -2,12 +2,15 @@
2 2
 
3 3
 namespace App\Filament\Company\Resources\Common\VendorResource\Pages;
4 4
 
5
+use App\Concerns\RedirectToListPage;
5 6
 use App\Filament\Company\Resources\Common\VendorResource;
6 7
 use Filament\Resources\Pages\CreateRecord;
7 8
 use Filament\Support\Enums\MaxWidth;
8 9
 
9 10
 class CreateVendor extends CreateRecord
10 11
 {
12
+    use RedirectToListPage;
13
+
11 14
     protected static string $resource = VendorResource::class;
12 15
 
13 16
     public function getMaxContentWidth(): MaxWidth | string | null

+ 3
- 0
app/Filament/Company/Resources/Common/VendorResource/Pages/EditVendor.php Прегледај датотеку

@@ -2,6 +2,7 @@
2 2
 
3 3
 namespace App\Filament\Company\Resources\Common\VendorResource\Pages;
4 4
 
5
+use App\Concerns\RedirectToListPage;
5 6
 use App\Filament\Company\Resources\Common\VendorResource;
6 7
 use Filament\Actions;
7 8
 use Filament\Resources\Pages\EditRecord;
@@ -9,6 +10,8 @@ use Filament\Support\Enums\MaxWidth;
9 10
 
10 11
 class EditVendor extends EditRecord
11 12
 {
13
+    use RedirectToListPage;
14
+
12 15
     protected static string $resource = VendorResource::class;
13 16
 
14 17
     protected function getHeaderActions(): array

+ 48
- 0
app/Filament/Company/Resources/Purchases/BillResource.php Прегледај датотеку

@@ -0,0 +1,48 @@
1
+<?php
2
+
3
+namespace App\Filament\Company\Resources\Purchases;
4
+
5
+use App\Filament\Company\Resources\Purchases\BillResource\Pages;
6
+use App\Models\Accounting\Bill;
7
+use Filament\Forms\Form;
8
+use Filament\Resources\Resource;
9
+use Filament\Tables\Table;
10
+
11
+class BillResource extends Resource
12
+{
13
+    protected static ?string $model = Bill::class;
14
+
15
+    protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
16
+
17
+    public static function form(Form $form): Form
18
+    {
19
+        return $form
20
+            ->schema([
21
+                //
22
+            ]);
23
+    }
24
+
25
+    public static function table(Table $table): Table
26
+    {
27
+        return $table
28
+            ->columns([
29
+                //
30
+            ]);
31
+    }
32
+
33
+    public static function getRelations(): array
34
+    {
35
+        return [
36
+            //
37
+        ];
38
+    }
39
+
40
+    public static function getPages(): array
41
+    {
42
+        return [
43
+            'index' => Pages\ListBills::route('/'),
44
+            'create' => Pages\CreateBill::route('/create'),
45
+            'edit' => Pages\EditBill::route('/{record}/edit'),
46
+        ];
47
+    }
48
+}

+ 11
- 0
app/Filament/Company/Resources/Purchases/BillResource/Pages/CreateBill.php Прегледај датотеку

@@ -0,0 +1,11 @@
1
+<?php
2
+
3
+namespace App\Filament\Company\Resources\Purchases\BillResource\Pages;
4
+
5
+use App\Filament\Company\Resources\Purchases\BillResource;
6
+use Filament\Resources\Pages\CreateRecord;
7
+
8
+class CreateBill extends CreateRecord
9
+{
10
+    protected static string $resource = BillResource::class;
11
+}

+ 19
- 0
app/Filament/Company/Resources/Purchases/BillResource/Pages/EditBill.php Прегледај датотеку

@@ -0,0 +1,19 @@
1
+<?php
2
+
3
+namespace App\Filament\Company\Resources\Purchases\BillResource\Pages;
4
+
5
+use App\Filament\Company\Resources\Purchases\BillResource;
6
+use Filament\Actions;
7
+use Filament\Resources\Pages\EditRecord;
8
+
9
+class EditBill extends EditRecord
10
+{
11
+    protected static string $resource = BillResource::class;
12
+
13
+    protected function getHeaderActions(): array
14
+    {
15
+        return [
16
+            Actions\DeleteAction::make(),
17
+        ];
18
+    }
19
+}

+ 19
- 0
app/Filament/Company/Resources/Purchases/BillResource/Pages/ListBills.php Прегледај датотеку

@@ -0,0 +1,19 @@
1
+<?php
2
+
3
+namespace App\Filament\Company\Resources\Purchases\BillResource\Pages;
4
+
5
+use App\Filament\Company\Resources\Purchases\BillResource;
6
+use Filament\Actions;
7
+use Filament\Resources\Pages\ListRecords;
8
+
9
+class ListBills extends ListRecords
10
+{
11
+    protected static string $resource = BillResource::class;
12
+
13
+    protected function getHeaderActions(): array
14
+    {
15
+        return [
16
+            Actions\CreateAction::make(),
17
+        ];
18
+    }
19
+}

+ 428
- 16
app/Filament/Company/Resources/Sales/InvoiceResource.php Прегледај датотеку

@@ -2,39 +2,451 @@
2 2
 
3 3
 namespace App\Filament\Company\Resources\Sales;
4 4
 
5
-use App\Enums\Accounting\DocumentType;
6
-use App\Filament\Company\Resources\Accounting\DocumentResource;
7 5
 use App\Filament\Company\Resources\Sales\InvoiceResource\Pages;
8
-use App\Models\Accounting\Document;
6
+use App\Models\Accounting\Adjustment;
7
+use App\Models\Accounting\DocumentLineItem;
8
+use App\Models\Accounting\Invoice;
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;
15
+use Filament\Forms\Components\FileUpload;
9 16
 use Filament\Forms\Form;
10 17
 use Filament\Resources\Resource;
18
+use Filament\Support\Enums\MaxWidth;
19
+use Filament\Tables;
11 20
 use Filament\Tables\Table;
12
-use Illuminate\Database\Eloquent\Builder;
21
+use Illuminate\Database\Eloquent\Model;
22
+use Illuminate\Support\Carbon;
23
+use Illuminate\Support\Facades\Auth;
24
+use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
13 25
 
14 26
 class InvoiceResource extends Resource
15 27
 {
16
-    protected static ?string $model = Document::class;
28
+    protected static ?string $model = Invoice::class;
17 29
 
18 30
     protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
19 31
 
20
-    protected static ?string $pluralModelLabel = 'Invoices';
32
+    public static function form(Form $form): Form
33
+    {
34
+        $company = Auth::user()->currentCompany;
21 35
 
22
-    protected static ?string $modelLabel = 'Invoice';
36
+        return $form
37
+            ->schema([
38
+                Forms\Components\Section::make('Invoice Header')
39
+                    ->collapsible()
40
+                    ->schema([
41
+                        Forms\Components\Split::make([
42
+                            Forms\Components\Group::make([
43
+                                FileUpload::make('logo')
44
+                                    ->openable()
45
+                                    ->maxSize(1024)
46
+                                    ->localizeLabel()
47
+                                    ->visibility('public')
48
+                                    ->disk('public')
49
+                                    ->directory('logos/document')
50
+                                    ->imageResizeMode('contain')
51
+                                    ->imageCropAspectRatio('3:2')
52
+                                    ->panelAspectRatio('3:2')
53
+                                    ->maxWidth(MaxWidth::ExtraSmall)
54
+                                    ->panelLayout('integrated')
55
+                                    ->removeUploadedFileButtonPosition('center bottom')
56
+                                    ->uploadButtonPosition('center bottom')
57
+                                    ->uploadProgressIndicatorPosition('center bottom')
58
+                                    ->getUploadedFileNameForStorageUsing(
59
+                                        static fn (TemporaryUploadedFile $file): string => (string) str($file->getClientOriginalName())
60
+                                            ->prepend(Auth::user()->currentCompany->id . '_'),
61
+                                    )
62
+                                    ->acceptedFileTypes(['image/png', 'image/jpeg', 'image/gif']),
63
+                            ]),
64
+                            Forms\Components\Group::make([
65
+                                Forms\Components\TextInput::make('header')
66
+                                    ->default(fn () => $company->defaultInvoice->header),
67
+                                Forms\Components\TextInput::make('subheader')
68
+                                    ->default(fn () => $company->defaultInvoice->subheader),
69
+                                Forms\Components\View::make('filament.forms.components.company-info')
70
+                                    ->viewData([
71
+                                        'company_name' => $company->name,
72
+                                        'company_address' => $company->profile->address,
73
+                                        'company_city' => $company->profile->city?->name,
74
+                                        'company_state' => $company->profile->state?->name,
75
+                                        'company_zip' => $company->profile->zip_code,
76
+                                        'company_country' => $company->profile->state?->country->name,
77
+                                    ]),
78
+                            ])->grow(true),
79
+                        ])->from('md'),
80
+                    ]),
81
+                Forms\Components\Section::make('Invoice Details')
82
+                    ->schema([
83
+                        Forms\Components\Split::make([
84
+                            Forms\Components\Group::make([
85
+                                Forms\Components\Select::make('client_id')
86
+                                    ->relationship('client', 'name')
87
+                                    ->preload()
88
+                                    ->searchable()
89
+                                    ->required(),
90
+                            ]),
91
+                            Forms\Components\Group::make([
92
+                                Forms\Components\TextInput::make('invoice_number')
93
+                                    ->label('Invoice Number')
94
+                                    ->default(fn () => Invoice::getNextDocumentNumber()),
95
+                                Forms\Components\TextInput::make('order_number')
96
+                                    ->label('P.O/S.O Number'),
97
+                                Forms\Components\DatePicker::make('date')
98
+                                    ->label('Invoice Date')
99
+                                    ->default(now()),
100
+                                Forms\Components\DatePicker::make('due_date')
101
+                                    ->label('Payment Due')
102
+                                    ->default(function () use ($company) {
103
+                                        return now()->addDays($company->defaultInvoice->payment_terms->getDays());
104
+                                    }),
105
+                            ])->grow(true),
106
+                        ])->from('md'),
107
+                        TableRepeater::make('lineItems')
108
+                            ->relationship()
109
+                            ->saveRelationshipsUsing(function (TableRepeater $component, Forms\Contracts\HasForms $livewire, ?array $state) {
110
+                                if (! is_array($state)) {
111
+                                    $state = [];
112
+                                }
23 113
 
24
-    public static function getEloquentQuery(): Builder
25
-    {
26
-        return parent::getEloquentQuery()
27
-            ->where('type', DocumentType::Invoice);
28
-    }
114
+                                $relationship = $component->getRelationship();
29 115
 
30
-    public static function form(Form $form): Form
31
-    {
32
-        return DocumentResource::form($form);
116
+                                $existingRecords = $component->getCachedExistingRecords();
117
+
118
+                                $recordsToDelete = [];
119
+
120
+                                foreach ($existingRecords->pluck($relationship->getRelated()->getKeyName()) as $keyToCheckForDeletion) {
121
+                                    if (array_key_exists("record-{$keyToCheckForDeletion}", $state)) {
122
+                                        continue;
123
+                                    }
124
+
125
+                                    $recordsToDelete[] = $keyToCheckForDeletion;
126
+                                    $existingRecords->forget("record-{$keyToCheckForDeletion}");
127
+                                }
128
+
129
+                                $relationship
130
+                                    ->whereKey($recordsToDelete)
131
+                                    ->get()
132
+                                    ->each(static fn (Model $record) => $record->delete());
133
+
134
+                                $childComponentContainers = $component->getChildComponentContainers(
135
+                                    withHidden: $component->shouldSaveRelationshipsWhenHidden(),
136
+                                );
137
+
138
+                                $itemOrder = 1;
139
+                                $orderColumn = $component->getOrderColumn();
140
+
141
+                                $translatableContentDriver = $livewire->makeFilamentTranslatableContentDriver();
142
+
143
+                                foreach ($childComponentContainers as $itemKey => $item) {
144
+                                    $itemData = $item->getState(shouldCallHooksBefore: false);
145
+
146
+                                    if ($orderColumn) {
147
+                                        $itemData[$orderColumn] = $itemOrder;
148
+
149
+                                        $itemOrder++;
150
+                                    }
151
+
152
+                                    if ($record = ($existingRecords[$itemKey] ?? null)) {
153
+                                        $itemData = $component->mutateRelationshipDataBeforeSave($itemData, record: $record);
154
+
155
+                                        if ($itemData === null) {
156
+                                            continue;
157
+                                        }
158
+
159
+                                        $translatableContentDriver ?
160
+                                            $translatableContentDriver->updateRecord($record, $itemData) :
161
+                                            $record->fill($itemData)->save();
162
+
163
+                                        continue;
164
+                                    }
165
+
166
+                                    $relatedModel = $component->getRelatedModel();
167
+
168
+                                    $itemData = $component->mutateRelationshipDataBeforeCreate($itemData);
169
+
170
+                                    if ($itemData === null) {
171
+                                        continue;
172
+                                    }
173
+
174
+                                    if ($translatableContentDriver) {
175
+                                        $record = $translatableContentDriver->makeRecord($relatedModel, $itemData);
176
+                                    } else {
177
+                                        $record = new $relatedModel;
178
+                                        $record->fill($itemData);
179
+                                    }
180
+
181
+                                    $record = $relationship->save($record);
182
+                                    $item->model($record)->saveRelationships();
183
+                                    $existingRecords->push($record);
184
+                                }
185
+
186
+                                $component->getRecord()->setRelation($component->getRelationshipName(), $existingRecords);
187
+
188
+                                /** @var Invoice $invoice */
189
+                                $invoice = $component->getRecord();
190
+
191
+                                // Recalculate totals for line items
192
+                                $invoice->lineItems()->each(function (DocumentLineItem $lineItem) {
193
+                                    $lineItem->updateQuietly([
194
+                                        'tax_total' => $lineItem->calculateTaxTotal()->getAmount(),
195
+                                        'discount_total' => $lineItem->calculateDiscountTotal()->getAmount(),
196
+                                    ]);
197
+                                });
198
+
199
+                                $subtotal = $invoice->lineItems()->sum('subtotal') / 100;
200
+                                $taxTotal = $invoice->lineItems()->sum('tax_total') / 100;
201
+                                $discountTotal = $invoice->lineItems()->sum('discount_total') / 100;
202
+                                $grandTotal = $subtotal + $taxTotal - $discountTotal;
203
+
204
+                                $invoice->updateQuietly([
205
+                                    'subtotal' => $subtotal,
206
+                                    'tax_total' => $taxTotal,
207
+                                    'discount_total' => $discountTotal,
208
+                                    'total' => $grandTotal,
209
+                                ]);
210
+                            })
211
+                            ->headers([
212
+                                Header::make('Items')->width('15%'),
213
+                                Header::make('Description')->width('25%'),
214
+                                Header::make('Quantity')->width('10%'),
215
+                                Header::make('Price')->width('10%'),
216
+                                Header::make('Taxes')->width('15%'),
217
+                                Header::make('Discounts')->width('15%'),
218
+                                Header::make('Amount')->width('10%')->align('right'),
219
+                            ])
220
+                            ->schema([
221
+                                Forms\Components\Select::make('offering_id')
222
+                                    ->relationship('offering', 'name')
223
+                                    ->preload()
224
+                                    ->searchable()
225
+                                    ->required()
226
+                                    ->live()
227
+                                    ->afterStateUpdated(function (Forms\Set $set, Forms\Get $get, $state) {
228
+                                        $offeringId = $state;
229
+                                        $offeringRecord = Offering::with('salesTaxes')->find($offeringId);
230
+
231
+                                        if ($offeringRecord) {
232
+                                            $set('description', $offeringRecord->description);
233
+                                            $set('unit_price', $offeringRecord->price);
234
+                                            $set('salesTaxes', $offeringRecord->salesTaxes->pluck('id')->toArray());
235
+                                            $set('salesDiscounts', $offeringRecord->salesDiscounts->pluck('id')->toArray());
236
+                                        }
237
+                                    }),
238
+                                Forms\Components\TextInput::make('description'),
239
+                                Forms\Components\TextInput::make('quantity')
240
+                                    ->required()
241
+                                    ->numeric()
242
+                                    ->live()
243
+                                    ->default(1),
244
+                                Forms\Components\TextInput::make('unit_price')
245
+                                    ->hiddenLabel()
246
+                                    ->numeric()
247
+                                    ->live()
248
+                                    ->default(0),
249
+                                Forms\Components\Select::make('salesTaxes')
250
+                                    ->relationship('salesTaxes', 'name')
251
+                                    ->preload()
252
+                                    ->multiple()
253
+                                    ->live()
254
+                                    ->searchable(),
255
+                                Forms\Components\Select::make('salesDiscounts')
256
+                                    ->relationship('salesDiscounts', 'name')
257
+                                    ->preload()
258
+                                    ->multiple()
259
+                                    ->live()
260
+                                    ->searchable(),
261
+                                Forms\Components\Placeholder::make('total')
262
+                                    ->hiddenLabel()
263
+                                    ->content(function (Forms\Get $get) {
264
+                                        $quantity = max((float) ($get('quantity') ?? 0), 0);
265
+                                        $unitPrice = max((float) ($get('unit_price') ?? 0), 0);
266
+                                        $salesTaxes = $get('salesTaxes') ?? [];
267
+                                        $salesDiscounts = $get('salesDiscounts') ?? [];
268
+
269
+                                        $subtotal = $quantity * $unitPrice;
270
+
271
+                                        // Calculate tax amount based on subtotal
272
+                                        $taxAmount = 0;
273
+                                        if (! empty($salesTaxes)) {
274
+                                            $taxRates = Adjustment::whereIn('id', $salesTaxes)->pluck('rate');
275
+                                            $taxAmount = collect($taxRates)->sum(fn ($rate) => $subtotal * ($rate / 100));
276
+                                        }
277
+
278
+                                        // Calculate discount amount based on subtotal
279
+                                        $discountAmount = 0;
280
+                                        if (! empty($salesDiscounts)) {
281
+                                            $discountRates = Adjustment::whereIn('id', $salesDiscounts)->pluck('rate');
282
+                                            $discountAmount = collect($discountRates)->sum(fn ($rate) => $subtotal * ($rate / 100));
283
+                                        }
284
+
285
+                                        // Final total
286
+                                        $total = $subtotal + ($taxAmount - $discountAmount);
287
+
288
+                                        return money($total, CurrencyAccessor::getDefaultCurrency(), true)->format();
289
+                                    }),
290
+                            ]),
291
+                        Forms\Components\Grid::make(6)
292
+                            ->schema([
293
+                                Forms\Components\ViewField::make('totals')
294
+                                    ->columnStart(5)
295
+                                    ->columnSpan(2)
296
+                                    ->view('filament.forms.components.invoice-totals'),
297
+                            ]),
298
+                        //                        Forms\Components\Repeater::make('lineItems')
299
+                        //                            ->relationship()
300
+                        //                            ->columns(8)
301
+                        //                            ->schema([
302
+                        //                                Forms\Components\Select::make('offering_id')
303
+                        //                                    ->relationship('offering', 'name')
304
+                        //                                    ->preload()
305
+                        //                                    ->columnSpan(2)
306
+                        //                                    ->searchable()
307
+                        //                                    ->required()
308
+                        //                                    ->live()
309
+                        //                                    ->afterStateUpdated(function (Forms\Set $set, $state) {
310
+                        //                                        $offeringId = $state;
311
+                        //                                        $offeringRecord = Offering::with('salesTaxes')->find($offeringId);
312
+                        //
313
+                        //                                        if ($offeringRecord) {
314
+                        //                                            $set('description', $offeringRecord->description);
315
+                        //                                            $set('unit_price', $offeringRecord->price);
316
+                        //                                            $set('total', $offeringRecord->price);
317
+                        //
318
+                        //                                            $salesTaxes = $offeringRecord->salesTaxes->map(function ($tax) {
319
+                        //                                                return [
320
+                        //                                                    'id' => $tax->id,
321
+                        //                                                    'amount' => null, // Amount will be calculated dynamically
322
+                        //                                                ];
323
+                        //                                            })->toArray();
324
+                        //
325
+                        //                                            $set('taxes', $salesTaxes);
326
+                        //                                        }
327
+                        //                                    }),
328
+                        //                                Forms\Components\TextInput::make('description')
329
+                        //                                    ->columnSpan(3)
330
+                        //                                    ->required(),
331
+                        //                                Forms\Components\TextInput::make('quantity')
332
+                        //                                    ->required()
333
+                        //                                    ->numeric()
334
+                        //                                    ->live()
335
+                        //                                    ->default(1),
336
+                        //                                Forms\Components\TextInput::make('unit_price')
337
+                        //                                    ->live()
338
+                        //                                    ->numeric()
339
+                        //                                    ->default(0),
340
+                        //                                Forms\Components\Placeholder::make('total')
341
+                        //                                    ->content(function (Forms\Get $get) {
342
+                        //                                        $quantity = $get('quantity');
343
+                        //                                        $unitPrice = $get('unit_price');
344
+                        //
345
+                        //                                        if ($quantity && $unitPrice) {
346
+                        //                                            return $quantity * $unitPrice;
347
+                        //                                        }
348
+                        //                                    }),
349
+                        //                                TableRepeater::make('taxes')
350
+                        //                                    ->relationship()
351
+                        //                                    ->columnSpanFull()
352
+                        //                                    ->columnStart(6)
353
+                        //                                    ->headers([
354
+                        //                                        Header::make('')->width('200px'),
355
+                        //                                        Header::make('')->width('50px')->align('right'),
356
+                        //                                    ])
357
+                        //                                    ->defaultItems(0)
358
+                        //                                    ->schema([
359
+                        //                                        Forms\Components\Select::make('id') // The ID of the adjustment being attached.
360
+                        //                                            ->label('Tax Adjustment')
361
+                        //                                            ->options(
362
+                        //                                                Adjustment::query()
363
+                        //                                                    ->where('category', AdjustmentCategory::Tax)
364
+                        //                                                    ->pluck('name', 'id')
365
+                        //                                            )
366
+                        //                                            ->preload()
367
+                        //                                            ->searchable()
368
+                        //                                            ->required()
369
+                        //                                            ->live(),
370
+                        //                                        Forms\Components\Placeholder::make('amount')
371
+                        //                                            ->hiddenLabel()
372
+                        //                                            ->content(function (Forms\Get $get) {
373
+                        //                                                $quantity = $get('../../quantity') ?? 0; // Get parent quantity
374
+                        //                                                $unitPrice = $get('../../unit_price') ?? 0; // Get parent unit price
375
+                        //                                                $rate = Adjustment::find($get('id'))->rate ?? 0;
376
+                        //
377
+                        //                                                $total = $quantity * $unitPrice;
378
+                        //
379
+                        //                                                return $total * ($rate / 100);
380
+                        //                                            }),
381
+                        //                                    ]),
382
+                        //                            ]),
383
+                        Forms\Components\Textarea::make('terms')
384
+                            ->columnSpanFull(),
385
+                    ]),
386
+                Forms\Components\Section::make('Invoice Footer')
387
+                    ->collapsible()
388
+                    ->schema([
389
+                        Forms\Components\Textarea::make('footer')
390
+                            ->columnSpanFull(),
391
+                    ]),
392
+            ]);
33 393
     }
34 394
 
35 395
     public static function table(Table $table): Table
36 396
     {
37
-        return DocumentResource::table($table);
397
+        return $table
398
+            ->columns([
399
+                Tables\Columns\TextColumn::make('status')
400
+                    ->badge()
401
+                    ->searchable(),
402
+                Tables\Columns\TextColumn::make('due_date')
403
+                    ->label('Due')
404
+                    ->formatStateUsing(function (Tables\Columns\TextColumn $column, mixed $state) {
405
+                        if (blank($state)) {
406
+                            return null;
407
+                        }
408
+
409
+                        $date = Carbon::parse($state)
410
+                            ->setTimezone($timezone ?? $column->getTimezone());
411
+
412
+                        if ($date->isToday()) {
413
+                            return 'Today';
414
+                        }
415
+
416
+                        return $date->diffForHumans([
417
+                            'options' => CarbonInterface::ONE_DAY_WORDS,
418
+                        ]);
419
+                    })
420
+                    ->sortable(),
421
+                Tables\Columns\TextColumn::make('date')
422
+                    ->date()
423
+                    ->sortable(),
424
+                Tables\Columns\TextColumn::make('invoice_number')
425
+                    ->label('Number')
426
+                    ->searchable(),
427
+                Tables\Columns\TextColumn::make('client.name')
428
+                    ->numeric()
429
+                    ->sortable(),
430
+                Tables\Columns\TextColumn::make('total')
431
+                    ->currency(),
432
+                Tables\Columns\TextColumn::make('amount_paid')
433
+                    ->label('Amount Paid')
434
+                    ->currency(),
435
+                Tables\Columns\TextColumn::make('amount_due')
436
+                    ->label('Amount Due')
437
+                    ->currency(),
438
+            ])
439
+            ->filters([
440
+                //
441
+            ])
442
+            ->actions([
443
+                Tables\Actions\EditAction::make(),
444
+            ])
445
+            ->bulkActions([
446
+                Tables\Actions\BulkActionGroup::make([
447
+                    Tables\Actions\DeleteBulkAction::make(),
448
+                ]),
449
+            ]);
38 450
     }
39 451
 
40 452
     public static function getRelations(): array

+ 3
- 7
app/Filament/Company/Resources/Sales/InvoiceResource/Pages/CreateInvoice.php Прегледај датотеку

@@ -2,23 +2,19 @@
2 2
 
3 3
 namespace App\Filament\Company\Resources\Sales\InvoiceResource\Pages;
4 4
 
5
+use App\Concerns\RedirectToListPage;
5 6
 use App\Filament\Company\Resources\Sales\InvoiceResource;
6 7
 use Filament\Resources\Pages\CreateRecord;
7 8
 use Filament\Support\Enums\MaxWidth;
8 9
 
9 10
 class CreateInvoice extends CreateRecord
10 11
 {
12
+    use RedirectToListPage;
13
+
11 14
     protected static string $resource = InvoiceResource::class;
12 15
 
13 16
     public function getMaxContentWidth(): MaxWidth | string | null
14 17
     {
15 18
         return MaxWidth::Full;
16 19
     }
17
-
18
-    protected function mutateFormDataBeforeCreate(array $data): array
19
-    {
20
-        $data['type'] = 'invoice';
21
-
22
-        return parent::mutateFormDataBeforeCreate($data);
23
-    }
24 20
 }

+ 3
- 0
app/Filament/Company/Resources/Sales/InvoiceResource/Pages/EditInvoice.php Прегледај датотеку

@@ -2,6 +2,7 @@
2 2
 
3 3
 namespace App\Filament\Company\Resources\Sales\InvoiceResource\Pages;
4 4
 
5
+use App\Concerns\RedirectToListPage;
5 6
 use App\Filament\Company\Resources\Sales\InvoiceResource;
6 7
 use Filament\Actions;
7 8
 use Filament\Resources\Pages\EditRecord;
@@ -9,6 +10,8 @@ use Filament\Support\Enums\MaxWidth;
9 10
 
10 11
 class EditInvoice extends EditRecord
11 12
 {
13
+    use RedirectToListPage;
14
+
12 15
     protected static string $resource = InvoiceResource::class;
13 16
 
14 17
     protected function getHeaderActions(): array

+ 68
- 0
app/Models/Accounting/Bill.php Прегледај датотеку

@@ -0,0 +1,68 @@
1
+<?php
2
+
3
+namespace App\Models\Accounting;
4
+
5
+use App\Casts\MoneyCast;
6
+use App\Concerns\Blamable;
7
+use App\Concerns\CompanyOwned;
8
+use App\Enums\Accounting\BillStatus;
9
+use App\Models\Banking\Payment;
10
+use App\Models\Common\Vendor;
11
+use Illuminate\Database\Eloquent\Factories\HasFactory;
12
+use Illuminate\Database\Eloquent\Model;
13
+use Illuminate\Database\Eloquent\Relations\BelongsTo;
14
+use Illuminate\Database\Eloquent\Relations\MorphMany;
15
+
16
+class Bill extends Model
17
+{
18
+    use Blamable;
19
+    use CompanyOwned;
20
+    use HasFactory;
21
+
22
+    protected $table = 'bills';
23
+
24
+    protected $fillable = [
25
+        'company_id',
26
+        'vendor_id',
27
+        'bill_number',
28
+        'order_number',
29
+        'date',
30
+        'due_date',
31
+        'status',
32
+        'currency_code',
33
+        'subtotal',
34
+        'tax_total',
35
+        'discount_total',
36
+        'total',
37
+        'amount_paid',
38
+        'created_by',
39
+        'updated_by',
40
+    ];
41
+
42
+    protected $casts = [
43
+        'date' => 'date',
44
+        'due_date' => 'date',
45
+        'status' => BillStatus::class,
46
+        'subtotal' => MoneyCast::class,
47
+        'tax_total' => MoneyCast::class,
48
+        'discount_total' => MoneyCast::class,
49
+        'total' => MoneyCast::class,
50
+        'amount_paid' => MoneyCast::class,
51
+        'amount_due' => MoneyCast::class,
52
+    ];
53
+
54
+    public function vendor(): BelongsTo
55
+    {
56
+        return $this->belongsTo(Vendor::class);
57
+    }
58
+
59
+    public function lineItems(): MorphMany
60
+    {
61
+        return $this->morphMany(DocumentLineItem::class, 'documentable');
62
+    }
63
+
64
+    public function payments(): MorphMany
65
+    {
66
+        return $this->morphMany(Payment::class, 'payable');
67
+    }
68
+}

+ 3
- 3
app/Models/Accounting/DocumentLineItem.php Прегледај датотеку

@@ -16,6 +16,7 @@ use Illuminate\Database\Eloquent\Attributes\ObservedBy;
16 16
 use Illuminate\Database\Eloquent\Factories\HasFactory;
17 17
 use Illuminate\Database\Eloquent\Model;
18 18
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
19
+use Illuminate\Database\Eloquent\Relations\MorphTo;
19 20
 use Illuminate\Database\Eloquent\Relations\MorphToMany;
20 21
 
21 22
 #[ObservedBy(DocumentLineItemObserver::class)]
@@ -29,7 +30,6 @@ class DocumentLineItem extends Model
29 30
 
30 31
     protected $fillable = [
31 32
         'company_id',
32
-        'document_id',
33 33
         'offering_id',
34 34
         'description',
35 35
         'quantity',
@@ -48,9 +48,9 @@ class DocumentLineItem extends Model
48 48
         'total' => MoneyCast::class,
49 49
     ];
50 50
 
51
-    public function document(): BelongsTo
51
+    public function documentable(): MorphTo
52 52
     {
53
-        return $this->belongsTo(Document::class);
53
+        return $this->morphTo();
54 54
     }
55 55
 
56 56
     public function offering(): BelongsTo

app/Models/Accounting/Document.php → app/Models/Accounting/Invoice.php Прегледај датотеку

@@ -5,36 +5,29 @@ namespace App\Models\Accounting;
5 5
 use App\Casts\MoneyCast;
6 6
 use App\Concerns\Blamable;
7 7
 use App\Concerns\CompanyOwned;
8
-use App\Enums\Accounting\DocumentStatus;
9
-use App\Enums\Accounting\DocumentType;
8
+use App\Enums\Accounting\InvoiceStatus;
10 9
 use App\Models\Banking\Payment;
11 10
 use App\Models\Common\Client;
12
-use App\Models\Common\Vendor;
13
-use App\Observers\DocumentObserver;
14
-use Illuminate\Database\Eloquent\Attributes\ObservedBy;
15 11
 use Illuminate\Database\Eloquent\Factories\HasFactory;
16 12
 use Illuminate\Database\Eloquent\Model;
17 13
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
18
-use Illuminate\Database\Eloquent\Relations\HasMany;
14
+use Illuminate\Database\Eloquent\Relations\MorphMany;
19 15
 
20
-#[ObservedBy(DocumentObserver::class)]
21
-class Document extends Model
16
+class Invoice extends Model
22 17
 {
23 18
     use Blamable;
24 19
     use CompanyOwned;
25 20
     use HasFactory;
26 21
 
27
-    protected $table = 'documents';
22
+    protected $table = 'invoices';
28 23
 
29 24
     protected $fillable = [
30 25
         'company_id',
31 26
         'client_id',
32
-        'vendor_id',
33
-        'type',
34 27
         'logo',
35 28
         'header',
36 29
         'subheader',
37
-        'document_number',
30
+        'invoice_number',
38 31
         'order_number',
39 32
         'date',
40 33
         'due_date',
@@ -52,10 +45,9 @@ class Document extends Model
52 45
     ];
53 46
 
54 47
     protected $casts = [
55
-        'type' => DocumentType::class,
56 48
         'date' => 'date',
57 49
         'due_date' => 'date',
58
-        'status' => DocumentStatus::class,
50
+        'status' => InvoiceStatus::class,
59 51
         'subtotal' => MoneyCast::class,
60 52
         'tax_total' => MoneyCast::class,
61 53
         'discount_total' => MoneyCast::class,
@@ -69,22 +61,17 @@ class Document extends Model
69 61
         return $this->belongsTo(Client::class);
70 62
     }
71 63
 
72
-    public function vendor(): BelongsTo
64
+    public function lineItems(): MorphMany
73 65
     {
74
-        return $this->belongsTo(Vendor::class);
66
+        return $this->morphMany(DocumentLineItem::class, 'documentable');
75 67
     }
76 68
 
77
-    public function lineItems(): HasMany
69
+    public function payments(): MorphMany
78 70
     {
79
-        return $this->hasMany(DocumentLineItem::class);
71
+        return $this->morphMany(Payment::class, 'payable');
80 72
     }
81 73
 
82
-    public function payments(): HasMany
83
-    {
84
-        return $this->hasMany(Payment::class);
85
-    }
86
-
87
-    public static function getNextDocumentNumber(DocumentType $documentType = DocumentType::Invoice): string
74
+    public static function getNextDocumentNumber(): string
88 75
     {
89 76
         $company = auth()->user()->currentCompany;
90 77
 
@@ -98,9 +85,8 @@ class Document extends Model
98 85
         $numberDigits = $defaultInvoiceSettings->number_digits;
99 86
 
100 87
         $latestDocument = static::query()
101
-            ->whereNotNull('document_number')
102
-            ->where('type', $documentType)
103
-            ->latest('document_number')
88
+            ->whereNotNull('invoice_number')
89
+            ->latest('invoice_number')
104 90
             ->first();
105 91
 
106 92
         $lastNumberNumericPart = $latestDocument

+ 3
- 4
app/Models/Banking/Payment.php Прегледај датотеку

@@ -5,10 +5,10 @@ namespace App\Models\Banking;
5 5
 use App\Casts\MoneyCast;
6 6
 use App\Concerns\Blamable;
7 7
 use App\Concerns\CompanyOwned;
8
-use App\Models\Accounting\Document;
9 8
 use Illuminate\Database\Eloquent\Factories\HasFactory;
10 9
 use Illuminate\Database\Eloquent\Model;
11 10
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
11
+use Illuminate\Database\Eloquent\Relations\MorphTo;
12 12
 
13 13
 class Payment extends Model
14 14
 {
@@ -18,7 +18,6 @@ class Payment extends Model
18 18
 
19 19
     protected $fillable = [
20 20
         'company_id',
21
-        'document_id',
22 21
         'date',
23 22
         'amount',
24 23
         'payment_method',
@@ -33,9 +32,9 @@ class Payment extends Model
33 32
         'amount' => MoneyCast::class,
34 33
     ];
35 34
 
36
-    public function document(): BelongsTo
35
+    public function payable(): MorphTo
37 36
     {
38
-        return $this->belongsTo(Document::class);
37
+        return $this->morphTo();
39 38
     }
40 39
 
41 40
     public function bankAccount(): BelongsTo

+ 2
- 4
app/Models/Common/Client.php Прегледај датотеку

@@ -4,9 +4,8 @@ namespace App\Models\Common;
4 4
 
5 5
 use App\Concerns\Blamable;
6 6
 use App\Concerns\CompanyOwned;
7
-use App\Enums\Accounting\DocumentType;
8 7
 use App\Enums\Common\AddressType;
9
-use App\Models\Accounting\Document;
8
+use App\Models\Accounting\Invoice;
10 9
 use App\Models\Setting\Currency;
11 10
 use Illuminate\Database\Eloquent\Factories\HasFactory;
12 11
 use Illuminate\Database\Eloquent\Model;
@@ -75,7 +74,6 @@ class Client extends Model
75 74
 
76 75
     public function invoices(): HasMany
77 76
     {
78
-        return $this->hasMany(Document::class, 'client_id')
79
-            ->where('type', DocumentType::Invoice);
77
+        return $this->hasMany(Invoice::class);
80 78
     }
81 79
 }

+ 16
- 0
app/Models/Common/Contact.php Прегледај датотеку

@@ -77,6 +77,22 @@ class Contact extends Model
77 77
         });
78 78
     }
79 79
 
80
+    protected function firstAvailablePhone(): Attribute
81
+    {
82
+        return Attribute::get(function () {
83
+            $priority = ['primary', 'mobile', 'toll_free', 'fax'];
84
+
85
+            foreach ($priority as $type) {
86
+                $phone = $this->getPhoneByType($type);
87
+                if ($phone) {
88
+                    return $phone;
89
+                }
90
+            }
91
+
92
+            return null; // Return null if no phone numbers are available
93
+        });
94
+    }
95
+
80 96
     private function getPhoneByType(string $type): ?string
81 97
     {
82 98
         if (! is_array($this->phones)) {

+ 2
- 4
app/Models/Common/Vendor.php Прегледај датотеку

@@ -4,10 +4,9 @@ namespace App\Models\Common;
4 4
 
5 5
 use App\Concerns\Blamable;
6 6
 use App\Concerns\CompanyOwned;
7
-use App\Enums\Accounting\DocumentType;
8 7
 use App\Enums\Common\ContractorType;
9 8
 use App\Enums\Common\VendorType;
10
-use App\Models\Accounting\Document;
9
+use App\Models\Accounting\Bill;
11 10
 use App\Models\Setting\Currency;
12 11
 use Illuminate\Database\Eloquent\Factories\HasFactory;
13 12
 use Illuminate\Database\Eloquent\Model;
@@ -47,8 +46,7 @@ class Vendor extends Model
47 46
 
48 47
     public function bills(): HasMany
49 48
     {
50
-        return $this->hasMany(Document::class, 'vendor_id')
51
-            ->where('type', DocumentType::Bill);
49
+        return $this->hasMany(Bill::class);
52 50
     }
53 51
 
54 52
     public function currency(): BelongsTo

+ 12
- 2
app/Models/Company.php Прегледај датотеку

@@ -89,6 +89,11 @@ class Company extends FilamentCompaniesCompany implements HasAvatar
89 89
         return $this->hasMany(BankAccount::class, 'company_id');
90 90
     }
91 91
 
92
+    public function bills(): HasMany
93
+    {
94
+        return $this->hasMany(Accounting\Bill::class, 'company_id');
95
+    }
96
+
92 97
     public function appearance(): HasOne
93 98
     {
94 99
         return $this->hasOne(Appearance::class, 'company_id');
@@ -137,9 +142,9 @@ class Company extends FilamentCompaniesCompany implements HasAvatar
137 142
         return $this->hasMany(Department::class, 'company_id');
138 143
     }
139 144
 
140
-    public function documents(): HasMany
145
+    public function invoices(): HasMany
141 146
     {
142
-        return $this->hasMany(Accounting\Document::class, 'company_id');
147
+        return $this->hasMany(Accounting\Invoice::class, 'company_id');
143 148
     }
144 149
 
145 150
     public function locale(): HasOne
@@ -147,6 +152,11 @@ class Company extends FilamentCompaniesCompany implements HasAvatar
147 152
         return $this->hasOne(Localization::class, 'company_id');
148 153
     }
149 154
 
155
+    public function payments(): HasMany
156
+    {
157
+        return $this->hasMany(Banking\Payment::class, 'company_id');
158
+    }
159
+
150 160
     public function profile(): HasOne
151 161
     {
152 162
         return $this->hasOne(CompanyProfile::class, 'company_id');

+ 0
- 68
app/Observers/DocumentObserver.php Прегледај датотеку

@@ -1,68 +0,0 @@
1
-<?php
2
-
3
-namespace App\Observers;
4
-
5
-use App\Models\Accounting\Document;
6
-
7
-class DocumentObserver
8
-{
9
-    /**
10
-     * Handle the Document "created" event.
11
-     */
12
-    public function created(Document $document): void
13
-    {
14
-        //
15
-    }
16
-
17
-    /**
18
-     * Handle the Document "deleted" event.
19
-     */
20
-    public function deleted(Document $document): void
21
-    {
22
-        //
23
-    }
24
-
25
-    /**
26
-     * Handle the Document "restored" event.
27
-     */
28
-    public function restored(Document $document): void
29
-    {
30
-        //
31
-    }
32
-
33
-    public function saved(Document $document): void
34
-    {
35
-        $this->recalculateTotals($document);
36
-    }
37
-
38
-    protected function recalculateTotals(Document $document): void
39
-    {
40
-        // Sum up values from line items
41
-        $subtotalCents = $document->lineItems()->sum('subtotal'); // Use the computed column directly
42
-        $taxTotalCents = $document->lineItems()->sum('tax_total'); // Sum from line items
43
-        $discountTotalCents = $document->lineItems()->sum('discount_total'); // Sum from line items
44
-        $grandTotalCents = $subtotalCents + $taxTotalCents - $discountTotalCents; // Calculated as before
45
-
46
-        // Convert from cents to dollars
47
-        $subtotal = $subtotalCents / 100;
48
-        $taxTotal = $taxTotalCents / 100;
49
-        $discountTotal = $discountTotalCents / 100;
50
-        $grandTotal = $grandTotalCents / 100;
51
-
52
-        // Update document totals
53
-        $document->updateQuietly([
54
-            'subtotal' => $subtotal, // Use database-computed subtotal
55
-            'tax_total' => $taxTotal,
56
-            'discount_total' => $discountTotal,
57
-            'total' => $grandTotal, // Use calculated grand total
58
-        ]);
59
-    }
60
-
61
-    /**
62
-     * Handle the Document "force deleted" event.
63
-     */
64
-    public function forceDeleted(Document $document): void
65
-    {
66
-        //
67
-    }
68
-}

+ 5
- 3
app/Providers/FilamentCompaniesServiceProvider.php Прегледај датотеку

@@ -25,11 +25,11 @@ use App\Filament\Company\Pages\ManageCompany;
25 25
 use App\Filament\Company\Pages\Reports;
26 26
 use App\Filament\Company\Pages\Service\ConnectedAccount;
27 27
 use App\Filament\Company\Pages\Service\LiveCurrency;
28
-use App\Filament\Company\Resources\Accounting\DocumentResource;
29 28
 use App\Filament\Company\Resources\Banking\AccountResource;
30 29
 use App\Filament\Company\Resources\Common\ClientResource;
31 30
 use App\Filament\Company\Resources\Common\OfferingResource;
32 31
 use App\Filament\Company\Resources\Common\VendorResource;
32
+use App\Filament\Company\Resources\Purchases\BillResource;
33 33
 use App\Filament\Company\Resources\Sales\InvoiceResource;
34 34
 use App\Filament\Components\PanelShiftDropdown;
35 35
 use App\Filament\User\Clusters\Account;
@@ -135,13 +135,15 @@ class FilamentCompaniesServiceProvider extends PanelProvider
135 135
                         NavigationGroup::make('Purchases')
136 136
                             ->label('Purchases')
137 137
                             ->icon('heroicon-o-shopping-cart')
138
-                            ->items(VendorResource::getNavigationItems()),
138
+                            ->items([
139
+                                ...BillResource::getNavigationItems(),
140
+                                ...VendorResource::getNavigationItems(),
141
+                            ]),
139 142
                         NavigationGroup::make('Accounting')
140 143
                             ->localizeLabel()
141 144
                             ->icon('heroicon-o-clipboard-document-list')
142 145
                             ->extraSidebarAttributes(['class' => 'es-sidebar-group'])
143 146
                             ->items([
144
-                                ...DocumentResource::getNavigationItems(),
145 147
                                 ...AccountChart::getNavigationItems(),
146 148
                                 ...Transactions::getNavigationItems(),
147 149
                             ]),

+ 1
- 1
app/Providers/MacroServiceProvider.php Прегледај датотеку

@@ -37,7 +37,7 @@ class MacroServiceProvider extends ServiceProvider
37 37
         TextInput::macro('money', function (string | Closure | null $currency = null): static {
38 38
             $currency ??= CurrencyAccessor::getDefaultCurrency();
39 39
 
40
-            $this->extraAttributes(['wire:key' => Str::random()])
40
+            $this
41 41
                 ->prefix(static function (TextInput $component) use ($currency) {
42 42
                     $currency = $component->evaluate($currency);
43 43
 

+ 16
- 16
app/View/Models/InvoiceTotalViewModel.php Прегледај датотеку

@@ -3,13 +3,13 @@
3 3
 namespace App\View\Models;
4 4
 
5 5
 use App\Models\Accounting\Adjustment;
6
-use App\Models\Accounting\Document;
6
+use App\Models\Accounting\Invoice;
7 7
 use App\Utilities\Currency\CurrencyConverter;
8 8
 
9 9
 class InvoiceTotalViewModel
10 10
 {
11 11
     public function __construct(
12
-        public ?Document $invoice,
12
+        public ?Invoice $invoice,
13 13
         public ?array $data = null
14 14
     ) {}
15 15
 
@@ -17,11 +17,16 @@ class InvoiceTotalViewModel
17 17
     {
18 18
         $lineItems = collect($this->data['lineItems'] ?? []);
19 19
 
20
-        $subtotal = $lineItems->sum(fn ($item) => ($item['quantity'] ?? 0) * ($item['unit_price'] ?? 0));
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
+        });
21 26
 
22 27
         $taxTotal = $lineItems->reduce(function ($carry, $item) {
23
-            $quantity = $item['quantity'] ?? 0;
24
-            $unitPrice = $item['unit_price'] ?? 0;
28
+            $quantity = max((float) ($item['quantity'] ?? 0), 0);
29
+            $unitPrice = max((float) ($item['unit_price'] ?? 0), 0);
25 30
             $salesTaxes = $item['salesTaxes'] ?? [];
26 31
             $lineTotal = $quantity * $unitPrice;
27 32
 
@@ -33,8 +38,8 @@ class InvoiceTotalViewModel
33 38
         }, 0);
34 39
 
35 40
         $discountTotal = $lineItems->reduce(function ($carry, $item) {
36
-            $quantity = $item['quantity'] ?? 0;
37
-            $unitPrice = $item['unit_price'] ?? 0;
41
+            $quantity = max((float) ($item['quantity'] ?? 0), 0);
42
+            $unitPrice = max((float) ($item['unit_price'] ?? 0), 0);
38 43
             $salesDiscounts = $item['salesDiscounts'] ?? [];
39 44
             $lineTotal = $quantity * $unitPrice;
40 45
 
@@ -47,16 +52,11 @@ class InvoiceTotalViewModel
47 52
 
48 53
         $grandTotal = $subtotal + ($taxTotal - $discountTotal);
49 54
 
50
-        $subTotalFormatted = CurrencyConverter::formatToMoney($subtotal);
51
-        $taxTotalFormatted = CurrencyConverter::formatToMoney($taxTotal);
52
-        $discountTotalFormatted = CurrencyConverter::formatToMoney($discountTotal);
53
-        $grandTotalFormatted = CurrencyConverter::formatToMoney($grandTotal);
54
-
55 55
         return [
56
-            'subtotal' => $subTotalFormatted,
57
-            'taxTotal' => $taxTotalFormatted,
58
-            'discountTotal' => $discountTotalFormatted,
59
-            'grandTotal' => $grandTotalFormatted,
56
+            'subtotal' => CurrencyConverter::formatToMoney($subtotal),
57
+            'taxTotal' => CurrencyConverter::formatToMoney($taxTotal),
58
+            'discountTotal' => CurrencyConverter::formatToMoney($discountTotal),
59
+            'grandTotal' => CurrencyConverter::formatToMoney($grandTotal),
60 60
         ];
61 61
     }
62 62
 }

+ 97
- 97
composer.lock Прегледај датотеку

@@ -497,16 +497,16 @@
497 497
         },
498 498
         {
499 499
             "name": "aws/aws-sdk-php",
500
-            "version": "3.330.2",
500
+            "version": "3.331.0",
501 501
             "source": {
502 502
                 "type": "git",
503 503
                 "url": "https://github.com/aws/aws-sdk-php.git",
504
-                "reference": "4ac43cc8356fb16a494c3631c8f39f6e7555f00a"
504
+                "reference": "0f8b3f63ba7b296afedcb3e6a43ce140831b9400"
505 505
             },
506 506
             "dist": {
507 507
                 "type": "zip",
508
-                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/4ac43cc8356fb16a494c3631c8f39f6e7555f00a",
509
-                "reference": "4ac43cc8356fb16a494c3631c8f39f6e7555f00a",
508
+                "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/0f8b3f63ba7b296afedcb3e6a43ce140831b9400",
509
+                "reference": "0f8b3f63ba7b296afedcb3e6a43ce140831b9400",
510 510
                 "shasum": ""
511 511
             },
512 512
             "require": {
@@ -535,7 +535,7 @@
535 535
                 "nette/neon": "^2.3",
536 536
                 "paragonie/random_compat": ">= 2",
537 537
                 "phpunit/phpunit": "^5.6.3 || ^8.5 || ^9.5",
538
-                "psr/cache": "^1.0",
538
+                "psr/cache": "^1.0 || ^2.0 || ^3.0",
539 539
                 "psr/simple-cache": "^1.0 || ^2.0 || ^3.0",
540 540
                 "sebastian/comparator": "^1.2.3 || ^4.0",
541 541
                 "yoast/phpunit-polyfills": "^1.0"
@@ -589,9 +589,9 @@
589 589
             "support": {
590 590
                 "forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
591 591
                 "issues": "https://github.com/aws/aws-sdk-php/issues",
592
-                "source": "https://github.com/aws/aws-sdk-php/tree/3.330.2"
592
+                "source": "https://github.com/aws/aws-sdk-php/tree/3.331.0"
593 593
             },
594
-            "time": "2024-11-26T19:07:56+00:00"
594
+            "time": "2024-11-27T19:12:58+00:00"
595 595
         },
596 596
         {
597 597
             "name": "aws/aws-sdk-php-laravel",
@@ -1664,16 +1664,16 @@
1664 1664
         },
1665 1665
         {
1666 1666
             "name": "filament/actions",
1667
-            "version": "v3.2.124",
1667
+            "version": "v3.2.125",
1668 1668
             "source": {
1669 1669
                 "type": "git",
1670 1670
                 "url": "https://github.com/filamentphp/actions.git",
1671
-                "reference": "631b38a36f5209a3884182acee60a0db682c6d24"
1671
+                "reference": "56d5d6f288cc5793b6da0cd85db45ffd98d06c6a"
1672 1672
             },
1673 1673
             "dist": {
1674 1674
                 "type": "zip",
1675
-                "url": "https://api.github.com/repos/filamentphp/actions/zipball/631b38a36f5209a3884182acee60a0db682c6d24",
1676
-                "reference": "631b38a36f5209a3884182acee60a0db682c6d24",
1675
+                "url": "https://api.github.com/repos/filamentphp/actions/zipball/56d5d6f288cc5793b6da0cd85db45ffd98d06c6a",
1676
+                "reference": "56d5d6f288cc5793b6da0cd85db45ffd98d06c6a",
1677 1677
                 "shasum": ""
1678 1678
             },
1679 1679
             "require": {
@@ -1713,20 +1713,20 @@
1713 1713
                 "issues": "https://github.com/filamentphp/filament/issues",
1714 1714
                 "source": "https://github.com/filamentphp/filament"
1715 1715
             },
1716
-            "time": "2024-11-13T16:35:31+00:00"
1716
+            "time": "2024-11-27T16:52:06+00:00"
1717 1717
         },
1718 1718
         {
1719 1719
             "name": "filament/filament",
1720
-            "version": "v3.2.124",
1720
+            "version": "v3.2.125",
1721 1721
             "source": {
1722 1722
                 "type": "git",
1723 1723
                 "url": "https://github.com/filamentphp/panels.git",
1724
-                "reference": "3f170b1c57033ad8e9e6bd71f3dc3f0665bf3ae9"
1724
+                "reference": "f76abb738d64cbb71fddb04d4c34e97b7e7ed86c"
1725 1725
             },
1726 1726
             "dist": {
1727 1727
                 "type": "zip",
1728
-                "url": "https://api.github.com/repos/filamentphp/panels/zipball/3f170b1c57033ad8e9e6bd71f3dc3f0665bf3ae9",
1729
-                "reference": "3f170b1c57033ad8e9e6bd71f3dc3f0665bf3ae9",
1728
+                "url": "https://api.github.com/repos/filamentphp/panels/zipball/f76abb738d64cbb71fddb04d4c34e97b7e7ed86c",
1729
+                "reference": "f76abb738d64cbb71fddb04d4c34e97b7e7ed86c",
1730 1730
                 "shasum": ""
1731 1731
             },
1732 1732
             "require": {
@@ -1778,20 +1778,20 @@
1778 1778
                 "issues": "https://github.com/filamentphp/filament/issues",
1779 1779
                 "source": "https://github.com/filamentphp/filament"
1780 1780
             },
1781
-            "time": "2024-11-13T16:35:35+00:00"
1781
+            "time": "2024-11-27T16:52:10+00:00"
1782 1782
         },
1783 1783
         {
1784 1784
             "name": "filament/forms",
1785
-            "version": "v3.2.124",
1785
+            "version": "v3.2.125",
1786 1786
             "source": {
1787 1787
                 "type": "git",
1788 1788
                 "url": "https://github.com/filamentphp/forms.git",
1789
-                "reference": "c73351c086036bd8de24e8671fd97018942d6d61"
1789
+                "reference": "7fadc5def6c9fbaf5ab68d1e65829ec4f2859a56"
1790 1790
             },
1791 1791
             "dist": {
1792 1792
                 "type": "zip",
1793
-                "url": "https://api.github.com/repos/filamentphp/forms/zipball/c73351c086036bd8de24e8671fd97018942d6d61",
1794
-                "reference": "c73351c086036bd8de24e8671fd97018942d6d61",
1793
+                "url": "https://api.github.com/repos/filamentphp/forms/zipball/7fadc5def6c9fbaf5ab68d1e65829ec4f2859a56",
1794
+                "reference": "7fadc5def6c9fbaf5ab68d1e65829ec4f2859a56",
1795 1795
                 "shasum": ""
1796 1796
             },
1797 1797
             "require": {
@@ -1834,11 +1834,11 @@
1834 1834
                 "issues": "https://github.com/filamentphp/filament/issues",
1835 1835
                 "source": "https://github.com/filamentphp/filament"
1836 1836
             },
1837
-            "time": "2024-11-13T16:35:31+00:00"
1837
+            "time": "2024-11-27T16:52:07+00:00"
1838 1838
         },
1839 1839
         {
1840 1840
             "name": "filament/infolists",
1841
-            "version": "v3.2.124",
1841
+            "version": "v3.2.125",
1842 1842
             "source": {
1843 1843
                 "type": "git",
1844 1844
                 "url": "https://github.com/filamentphp/infolists.git",
@@ -1889,7 +1889,7 @@
1889 1889
         },
1890 1890
         {
1891 1891
             "name": "filament/notifications",
1892
-            "version": "v3.2.124",
1892
+            "version": "v3.2.125",
1893 1893
             "source": {
1894 1894
                 "type": "git",
1895 1895
                 "url": "https://github.com/filamentphp/notifications.git",
@@ -1941,27 +1941,27 @@
1941 1941
         },
1942 1942
         {
1943 1943
             "name": "filament/support",
1944
-            "version": "v3.2.124",
1944
+            "version": "v3.2.125",
1945 1945
             "source": {
1946 1946
                 "type": "git",
1947 1947
                 "url": "https://github.com/filamentphp/support.git",
1948
-                "reference": "13b1e485d3bc993950c9e61a3f6a8cb05efd2b96"
1948
+                "reference": "4bd6a02e096742ec68d0ed955b285579b21d02a4"
1949 1949
             },
1950 1950
             "dist": {
1951 1951
                 "type": "zip",
1952
-                "url": "https://api.github.com/repos/filamentphp/support/zipball/13b1e485d3bc993950c9e61a3f6a8cb05efd2b96",
1953
-                "reference": "13b1e485d3bc993950c9e61a3f6a8cb05efd2b96",
1952
+                "url": "https://api.github.com/repos/filamentphp/support/zipball/4bd6a02e096742ec68d0ed955b285579b21d02a4",
1953
+                "reference": "4bd6a02e096742ec68d0ed955b285579b21d02a4",
1954 1954
                 "shasum": ""
1955 1955
             },
1956 1956
             "require": {
1957
-                "blade-ui-kit/blade-heroicons": "^2.2.1",
1957
+                "blade-ui-kit/blade-heroicons": "^2.5",
1958 1958
                 "doctrine/dbal": "^3.2|^4.0",
1959 1959
                 "ext-intl": "*",
1960 1960
                 "illuminate/contracts": "^10.45|^11.0",
1961 1961
                 "illuminate/support": "^10.45|^11.0",
1962 1962
                 "illuminate/view": "^10.45|^11.0",
1963 1963
                 "kirschbaum-development/eloquent-power-joins": "^3.0|^4.0",
1964
-                "livewire/livewire": "^3.4.10",
1964
+                "livewire/livewire": "3.5.12",
1965 1965
                 "php": "^8.1",
1966 1966
                 "ryangjchandler/blade-capture-directive": "^0.2|^0.3|^1.0",
1967 1967
                 "spatie/color": "^1.5",
@@ -1996,20 +1996,20 @@
1996 1996
                 "issues": "https://github.com/filamentphp/filament/issues",
1997 1997
                 "source": "https://github.com/filamentphp/filament"
1998 1998
             },
1999
-            "time": "2024-11-13T16:35:51+00:00"
1999
+            "time": "2024-11-27T16:52:24+00:00"
2000 2000
         },
2001 2001
         {
2002 2002
             "name": "filament/tables",
2003
-            "version": "v3.2.124",
2003
+            "version": "v3.2.125",
2004 2004
             "source": {
2005 2005
                 "type": "git",
2006 2006
                 "url": "https://github.com/filamentphp/tables.git",
2007
-                "reference": "5f1b04952080e71f3f72bae3801f2757619722e7"
2007
+                "reference": "85b23e7222e4b3df8531fc62b480a11e2dfb00c4"
2008 2008
             },
2009 2009
             "dist": {
2010 2010
                 "type": "zip",
2011
-                "url": "https://api.github.com/repos/filamentphp/tables/zipball/5f1b04952080e71f3f72bae3801f2757619722e7",
2012
-                "reference": "5f1b04952080e71f3f72bae3801f2757619722e7",
2011
+                "url": "https://api.github.com/repos/filamentphp/tables/zipball/85b23e7222e4b3df8531fc62b480a11e2dfb00c4",
2012
+                "reference": "85b23e7222e4b3df8531fc62b480a11e2dfb00c4",
2013 2013
                 "shasum": ""
2014 2014
             },
2015 2015
             "require": {
@@ -2048,20 +2048,20 @@
2048 2048
                 "issues": "https://github.com/filamentphp/filament/issues",
2049 2049
                 "source": "https://github.com/filamentphp/filament"
2050 2050
             },
2051
-            "time": "2024-11-13T16:35:47+00:00"
2051
+            "time": "2024-11-27T16:52:27+00:00"
2052 2052
         },
2053 2053
         {
2054 2054
             "name": "filament/widgets",
2055
-            "version": "v3.2.124",
2055
+            "version": "v3.2.125",
2056 2056
             "source": {
2057 2057
                 "type": "git",
2058 2058
                 "url": "https://github.com/filamentphp/widgets.git",
2059
-                "reference": "59a907af93c9027180e2bac5879f35b5fb11c96f"
2059
+                "reference": "6de1c84d71168fd1c6a5b1ae1e1b4ec5ee4b6f55"
2060 2060
             },
2061 2061
             "dist": {
2062 2062
                 "type": "zip",
2063
-                "url": "https://api.github.com/repos/filamentphp/widgets/zipball/59a907af93c9027180e2bac5879f35b5fb11c96f",
2064
-                "reference": "59a907af93c9027180e2bac5879f35b5fb11c96f",
2063
+                "url": "https://api.github.com/repos/filamentphp/widgets/zipball/6de1c84d71168fd1c6a5b1ae1e1b4ec5ee4b6f55",
2064
+                "reference": "6de1c84d71168fd1c6a5b1ae1e1b4ec5ee4b6f55",
2065 2065
                 "shasum": ""
2066 2066
             },
2067 2067
             "require": {
@@ -2092,7 +2092,7 @@
2092 2092
                 "issues": "https://github.com/filamentphp/filament/issues",
2093 2093
                 "source": "https://github.com/filamentphp/filament"
2094 2094
             },
2095
-            "time": "2024-11-13T16:35:48+00:00"
2095
+            "time": "2024-11-27T16:52:29+00:00"
2096 2096
         },
2097 2097
         {
2098 2098
             "name": "firebase/php-jwt",
@@ -2982,16 +2982,16 @@
2982 2982
         },
2983 2983
         {
2984 2984
             "name": "laravel/framework",
2985
-            "version": "v11.34.1",
2985
+            "version": "v11.34.2",
2986 2986
             "source": {
2987 2987
                 "type": "git",
2988 2988
                 "url": "https://github.com/laravel/framework.git",
2989
-                "reference": "ed07324892c87277b7d37ba76b1a6f93a37401b5"
2989
+                "reference": "865da6d73dd353f07a7bcbd778c55966a620121f"
2990 2990
             },
2991 2991
             "dist": {
2992 2992
                 "type": "zip",
2993
-                "url": "https://api.github.com/repos/laravel/framework/zipball/ed07324892c87277b7d37ba76b1a6f93a37401b5",
2994
-                "reference": "ed07324892c87277b7d37ba76b1a6f93a37401b5",
2993
+                "url": "https://api.github.com/repos/laravel/framework/zipball/865da6d73dd353f07a7bcbd778c55966a620121f",
2994
+                "reference": "865da6d73dd353f07a7bcbd778c55966a620121f",
2995 2995
                 "shasum": ""
2996 2996
             },
2997 2997
             "require": {
@@ -3191,7 +3191,7 @@
3191 3191
                 "issues": "https://github.com/laravel/framework/issues",
3192 3192
                 "source": "https://github.com/laravel/framework"
3193 3193
             },
3194
-            "time": "2024-11-26T21:32:01+00:00"
3194
+            "time": "2024-11-27T15:43:57+00:00"
3195 3195
         },
3196 3196
         {
3197 3197
             "name": "laravel/prompts",
@@ -4265,12 +4265,12 @@
4265 4265
             "type": "library",
4266 4266
             "extra": {
4267 4267
                 "laravel": {
4268
-                    "providers": [
4269
-                        "Livewire\\LivewireServiceProvider"
4270
-                    ],
4271 4268
                     "aliases": {
4272 4269
                         "Livewire": "Livewire\\Livewire"
4273
-                    }
4270
+                    },
4271
+                    "providers": [
4272
+                        "Livewire\\LivewireServiceProvider"
4273
+                    ]
4274 4274
                 }
4275 4275
             },
4276 4276
             "autoload": {
@@ -6821,16 +6821,16 @@
6821 6821
         },
6822 6822
         {
6823 6823
             "name": "symfony/deprecation-contracts",
6824
-            "version": "v3.5.0",
6824
+            "version": "v3.5.1",
6825 6825
             "source": {
6826 6826
                 "type": "git",
6827 6827
                 "url": "https://github.com/symfony/deprecation-contracts.git",
6828
-                "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1"
6828
+                "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6"
6829 6829
             },
6830 6830
             "dist": {
6831 6831
                 "type": "zip",
6832
-                "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
6833
-                "reference": "0e0d29ce1f20deffb4ab1b016a7257c4f1e789a1",
6832
+                "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
6833
+                "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6",
6834 6834
                 "shasum": ""
6835 6835
             },
6836 6836
             "require": {
@@ -6868,7 +6868,7 @@
6868 6868
             "description": "A generic function and convention to trigger deprecation notices",
6869 6869
             "homepage": "https://symfony.com",
6870 6870
             "support": {
6871
-                "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.0"
6871
+                "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1"
6872 6872
             },
6873 6873
             "funding": [
6874 6874
                 {
@@ -6884,7 +6884,7 @@
6884 6884
                     "type": "tidelift"
6885 6885
                 }
6886 6886
             ],
6887
-            "time": "2024-04-18T09:32:20+00:00"
6887
+            "time": "2024-09-25T14:20:29+00:00"
6888 6888
         },
6889 6889
         {
6890 6890
             "name": "symfony/error-handler",
@@ -7043,16 +7043,16 @@
7043 7043
         },
7044 7044
         {
7045 7045
             "name": "symfony/event-dispatcher-contracts",
7046
-            "version": "v3.5.0",
7046
+            "version": "v3.5.1",
7047 7047
             "source": {
7048 7048
                 "type": "git",
7049 7049
                 "url": "https://github.com/symfony/event-dispatcher-contracts.git",
7050
-                "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50"
7050
+                "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f"
7051 7051
             },
7052 7052
             "dist": {
7053 7053
                 "type": "zip",
7054
-                "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/8f93aec25d41b72493c6ddff14e916177c9efc50",
7055
-                "reference": "8f93aec25d41b72493c6ddff14e916177c9efc50",
7054
+                "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/7642f5e970b672283b7823222ae8ef8bbc160b9f",
7055
+                "reference": "7642f5e970b672283b7823222ae8ef8bbc160b9f",
7056 7056
                 "shasum": ""
7057 7057
             },
7058 7058
             "require": {
@@ -7099,7 +7099,7 @@
7099 7099
                 "standards"
7100 7100
             ],
7101 7101
             "support": {
7102
-                "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.0"
7102
+                "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.5.1"
7103 7103
             },
7104 7104
             "funding": [
7105 7105
                 {
@@ -7115,7 +7115,7 @@
7115 7115
                     "type": "tidelift"
7116 7116
                 }
7117 7117
             ],
7118
-            "time": "2024-04-18T09:32:20+00:00"
7118
+            "time": "2024-09-25T14:20:29+00:00"
7119 7119
         },
7120 7120
         {
7121 7121
             "name": "symfony/finder",
@@ -7252,16 +7252,16 @@
7252 7252
         },
7253 7253
         {
7254 7254
             "name": "symfony/http-foundation",
7255
-            "version": "v7.1.8",
7255
+            "version": "v7.1.9",
7256 7256
             "source": {
7257 7257
                 "type": "git",
7258 7258
                 "url": "https://github.com/symfony/http-foundation.git",
7259
-                "reference": "f4419ec69ccfc3f725a4de7c20e4e57626d10112"
7259
+                "reference": "82765842fb599c7ed839b650214680c7ee5779be"
7260 7260
             },
7261 7261
             "dist": {
7262 7262
                 "type": "zip",
7263
-                "url": "https://api.github.com/repos/symfony/http-foundation/zipball/f4419ec69ccfc3f725a4de7c20e4e57626d10112",
7264
-                "reference": "f4419ec69ccfc3f725a4de7c20e4e57626d10112",
7263
+                "url": "https://api.github.com/repos/symfony/http-foundation/zipball/82765842fb599c7ed839b650214680c7ee5779be",
7264
+                "reference": "82765842fb599c7ed839b650214680c7ee5779be",
7265 7265
                 "shasum": ""
7266 7266
             },
7267 7267
             "require": {
@@ -7309,7 +7309,7 @@
7309 7309
             "description": "Defines an object-oriented layer for the HTTP specification",
7310 7310
             "homepage": "https://symfony.com",
7311 7311
             "support": {
7312
-                "source": "https://github.com/symfony/http-foundation/tree/v7.1.8"
7312
+                "source": "https://github.com/symfony/http-foundation/tree/v7.1.9"
7313 7313
             },
7314 7314
             "funding": [
7315 7315
                 {
@@ -7325,20 +7325,20 @@
7325 7325
                     "type": "tidelift"
7326 7326
                 }
7327 7327
             ],
7328
-            "time": "2024-11-09T09:16:45+00:00"
7328
+            "time": "2024-11-13T18:58:36+00:00"
7329 7329
         },
7330 7330
         {
7331 7331
             "name": "symfony/http-kernel",
7332
-            "version": "v7.1.8",
7332
+            "version": "v7.1.9",
7333 7333
             "source": {
7334 7334
                 "type": "git",
7335 7335
                 "url": "https://github.com/symfony/http-kernel.git",
7336
-                "reference": "33fef24e3dc79d6d30bf4936531f2f4bd2ca189e"
7336
+                "reference": "649d0e23c571344ef1153d4ffb2564f534b85a45"
7337 7337
             },
7338 7338
             "dist": {
7339 7339
                 "type": "zip",
7340
-                "url": "https://api.github.com/repos/symfony/http-kernel/zipball/33fef24e3dc79d6d30bf4936531f2f4bd2ca189e",
7341
-                "reference": "33fef24e3dc79d6d30bf4936531f2f4bd2ca189e",
7340
+                "url": "https://api.github.com/repos/symfony/http-kernel/zipball/649d0e23c571344ef1153d4ffb2564f534b85a45",
7341
+                "reference": "649d0e23c571344ef1153d4ffb2564f534b85a45",
7342 7342
                 "shasum": ""
7343 7343
             },
7344 7344
             "require": {
@@ -7423,7 +7423,7 @@
7423 7423
             "description": "Provides a structured process for converting a Request into a Response",
7424 7424
             "homepage": "https://symfony.com",
7425 7425
             "support": {
7426
-                "source": "https://github.com/symfony/http-kernel/tree/v7.1.8"
7426
+                "source": "https://github.com/symfony/http-kernel/tree/v7.1.9"
7427 7427
             },
7428 7428
             "funding": [
7429 7429
                 {
@@ -7439,7 +7439,7 @@
7439 7439
                     "type": "tidelift"
7440 7440
                 }
7441 7441
             ],
7442
-            "time": "2024-11-13T14:25:32+00:00"
7442
+            "time": "2024-11-27T12:55:11+00:00"
7443 7443
         },
7444 7444
         {
7445 7445
             "name": "symfony/intl",
@@ -8387,16 +8387,16 @@
8387 8387
         },
8388 8388
         {
8389 8389
             "name": "symfony/routing",
8390
-            "version": "v7.1.6",
8390
+            "version": "v7.1.9",
8391 8391
             "source": {
8392 8392
                 "type": "git",
8393 8393
                 "url": "https://github.com/symfony/routing.git",
8394
-                "reference": "66a2c469f6c22d08603235c46a20007c0701ea0a"
8394
+                "reference": "a27bb8e0cc3ca4baf17159d053910c9736c3aa4c"
8395 8395
             },
8396 8396
             "dist": {
8397 8397
                 "type": "zip",
8398
-                "url": "https://api.github.com/repos/symfony/routing/zipball/66a2c469f6c22d08603235c46a20007c0701ea0a",
8399
-                "reference": "66a2c469f6c22d08603235c46a20007c0701ea0a",
8398
+                "url": "https://api.github.com/repos/symfony/routing/zipball/a27bb8e0cc3ca4baf17159d053910c9736c3aa4c",
8399
+                "reference": "a27bb8e0cc3ca4baf17159d053910c9736c3aa4c",
8400 8400
                 "shasum": ""
8401 8401
             },
8402 8402
             "require": {
@@ -8448,7 +8448,7 @@
8448 8448
                 "url"
8449 8449
             ],
8450 8450
             "support": {
8451
-                "source": "https://github.com/symfony/routing/tree/v7.1.6"
8451
+                "source": "https://github.com/symfony/routing/tree/v7.1.9"
8452 8452
             },
8453 8453
             "funding": [
8454 8454
                 {
@@ -8464,20 +8464,20 @@
8464 8464
                     "type": "tidelift"
8465 8465
                 }
8466 8466
             ],
8467
-            "time": "2024-10-01T08:31:23+00:00"
8467
+            "time": "2024-11-13T16:12:35+00:00"
8468 8468
         },
8469 8469
         {
8470 8470
             "name": "symfony/service-contracts",
8471
-            "version": "v3.5.0",
8471
+            "version": "v3.5.1",
8472 8472
             "source": {
8473 8473
                 "type": "git",
8474 8474
                 "url": "https://github.com/symfony/service-contracts.git",
8475
-                "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f"
8475
+                "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0"
8476 8476
             },
8477 8477
             "dist": {
8478 8478
                 "type": "zip",
8479
-                "url": "https://api.github.com/repos/symfony/service-contracts/zipball/bd1d9e59a81d8fa4acdcea3f617c581f7475a80f",
8480
-                "reference": "bd1d9e59a81d8fa4acdcea3f617c581f7475a80f",
8479
+                "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0",
8480
+                "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0",
8481 8481
                 "shasum": ""
8482 8482
             },
8483 8483
             "require": {
@@ -8531,7 +8531,7 @@
8531 8531
                 "standards"
8532 8532
             ],
8533 8533
             "support": {
8534
-                "source": "https://github.com/symfony/service-contracts/tree/v3.5.0"
8534
+                "source": "https://github.com/symfony/service-contracts/tree/v3.5.1"
8535 8535
             },
8536 8536
             "funding": [
8537 8537
                 {
@@ -8547,7 +8547,7 @@
8547 8547
                     "type": "tidelift"
8548 8548
                 }
8549 8549
             ],
8550
-            "time": "2024-04-18T09:32:20+00:00"
8550
+            "time": "2024-09-25T14:20:29+00:00"
8551 8551
         },
8552 8552
         {
8553 8553
             "name": "symfony/string",
@@ -8732,16 +8732,16 @@
8732 8732
         },
8733 8733
         {
8734 8734
             "name": "symfony/translation-contracts",
8735
-            "version": "v3.5.0",
8735
+            "version": "v3.5.1",
8736 8736
             "source": {
8737 8737
                 "type": "git",
8738 8738
                 "url": "https://github.com/symfony/translation-contracts.git",
8739
-                "reference": "b9d2189887bb6b2e0367a9fc7136c5239ab9b05a"
8739
+                "reference": "4667ff3bd513750603a09c8dedbea942487fb07c"
8740 8740
             },
8741 8741
             "dist": {
8742 8742
                 "type": "zip",
8743
-                "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/b9d2189887bb6b2e0367a9fc7136c5239ab9b05a",
8744
-                "reference": "b9d2189887bb6b2e0367a9fc7136c5239ab9b05a",
8743
+                "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/4667ff3bd513750603a09c8dedbea942487fb07c",
8744
+                "reference": "4667ff3bd513750603a09c8dedbea942487fb07c",
8745 8745
                 "shasum": ""
8746 8746
             },
8747 8747
             "require": {
@@ -8790,7 +8790,7 @@
8790 8790
                 "standards"
8791 8791
             ],
8792 8792
             "support": {
8793
-                "source": "https://github.com/symfony/translation-contracts/tree/v3.5.0"
8793
+                "source": "https://github.com/symfony/translation-contracts/tree/v3.5.1"
8794 8794
             },
8795 8795
             "funding": [
8796 8796
                 {
@@ -8806,7 +8806,7 @@
8806 8806
                     "type": "tidelift"
8807 8807
                 }
8808 8808
             ],
8809
-            "time": "2024-04-18T09:32:20+00:00"
8809
+            "time": "2024-09-25T14:20:29+00:00"
8810 8810
         },
8811 8811
         {
8812 8812
             "name": "symfony/uid",
@@ -9702,16 +9702,16 @@
9702 9702
         },
9703 9703
         {
9704 9704
             "name": "laravel/sail",
9705
-            "version": "v1.39.0",
9705
+            "version": "v1.39.1",
9706 9706
             "source": {
9707 9707
                 "type": "git",
9708 9708
                 "url": "https://github.com/laravel/sail.git",
9709
-                "reference": "be9d67a11133535811f9ec4ab5c176a2f47250fc"
9709
+                "reference": "1a3c7291bc88de983b66688919a4d298d68ddec7"
9710 9710
             },
9711 9711
             "dist": {
9712 9712
                 "type": "zip",
9713
-                "url": "https://api.github.com/repos/laravel/sail/zipball/be9d67a11133535811f9ec4ab5c176a2f47250fc",
9714
-                "reference": "be9d67a11133535811f9ec4ab5c176a2f47250fc",
9713
+                "url": "https://api.github.com/repos/laravel/sail/zipball/1a3c7291bc88de983b66688919a4d298d68ddec7",
9714
+                "reference": "1a3c7291bc88de983b66688919a4d298d68ddec7",
9715 9715
                 "shasum": ""
9716 9716
             },
9717 9717
             "require": {
@@ -9761,7 +9761,7 @@
9761 9761
                 "issues": "https://github.com/laravel/sail/issues",
9762 9762
                 "source": "https://github.com/laravel/sail"
9763 9763
             },
9764
-            "time": "2024-11-25T23:48:26+00:00"
9764
+            "time": "2024-11-27T15:42:28+00:00"
9765 9765
         },
9766 9766
         {
9767 9767
             "name": "mockery/mockery",

database/factories/Accounting/DocumentFactory.php → database/factories/Accounting/BillFactory.php Прегледај датотеку

@@ -5,9 +5,9 @@ namespace Database\Factories\Accounting;
5 5
 use Illuminate\Database\Eloquent\Factories\Factory;
6 6
 
7 7
 /**
8
- * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Accounting\Document>
8
+ * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Accounting\Bill>
9 9
  */
10
-class DocumentFactory extends Factory
10
+class BillFactory extends Factory
11 11
 {
12 12
     /**
13 13
      * Define the model's default state.

+ 23
- 0
database/factories/Accounting/InvoiceFactory.php Прегледај датотеку

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

+ 42
- 2
database/factories/Common/AddressFactory.php Прегледај датотеку

@@ -2,13 +2,20 @@
2 2
 
3 3
 namespace Database\Factories\Common;
4 4
 
5
+use App\Enums\Common\AddressType;
6
+use App\Models\Common\Address;
5 7
 use Illuminate\Database\Eloquent\Factories\Factory;
6 8
 
7 9
 /**
8
- * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Common\Address>
10
+ * @extends Factory<Address>
9 11
  */
10 12
 class AddressFactory extends Factory
11 13
 {
14
+    /**
15
+     * The name of the factory's corresponding model.
16
+     */
17
+    protected $model = Address::class;
18
+
12 19
     /**
13 20
      * Define the model's default state.
14 21
      *
@@ -17,7 +24,40 @@ class AddressFactory extends Factory
17 24
     public function definition(): array
18 25
     {
19 26
         return [
20
-            //
27
+            'company_id' => 1,
28
+            'type' => $this->faker->randomElement(AddressType::cases()),
29
+            'recipient' => $this->faker->name,
30
+            'phone' => $this->faker->phoneNumber,
31
+            'address_line_1' => $this->faker->streetAddress,
32
+            'address_line_2' => $this->faker->streetAddress,
33
+            'city' => $this->faker->city,
34
+            'state' => $this->faker->state('US'),
35
+            'postal_code' => $this->faker->postcode,
36
+            'country' => 'US',
37
+            'notes' => $this->faker->sentence,
38
+            'created_by' => 1,
39
+            'updated_by' => 1,
21 40
         ];
22 41
     }
42
+
43
+    public function billing(): self
44
+    {
45
+        return $this->state([
46
+            'type' => AddressType::Billing,
47
+        ]);
48
+    }
49
+
50
+    public function shipping(): self
51
+    {
52
+        return $this->state([
53
+            'type' => AddressType::Shipping,
54
+        ]);
55
+    }
56
+
57
+    public function general(): self
58
+    {
59
+        return $this->state([
60
+            'type' => AddressType::General,
61
+        ]);
62
+    }
23 63
 }

+ 34
- 2
database/factories/Common/ClientFactory.php Прегледај датотеку

@@ -2,13 +2,21 @@
2 2
 
3 3
 namespace Database\Factories\Common;
4 4
 
5
+use App\Models\Common\Address;
6
+use App\Models\Common\Client;
7
+use App\Models\Common\Contact;
5 8
 use Illuminate\Database\Eloquent\Factories\Factory;
6 9
 
7 10
 /**
8
- * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Common\Client>
11
+ * @extends Factory<Client>
9 12
  */
10 13
 class ClientFactory extends Factory
11 14
 {
15
+    /**
16
+     * The name of the factory's corresponding model.
17
+     */
18
+    protected $model = Client::class;
19
+
12 20
     /**
13 21
      * Define the model's default state.
14 22
      *
@@ -17,7 +25,31 @@ class ClientFactory extends Factory
17 25
     public function definition(): array
18 26
     {
19 27
         return [
20
-            //
28
+            'company_id' => 1,
29
+            'name' => $this->faker->company,
30
+            'currency_code' => 'USD',
31
+            'account_number' => $this->faker->unique()->numerify(str_repeat('#', 12)),
32
+            'website' => $this->faker->url,
33
+            'notes' => $this->faker->sentence,
34
+            'created_by' => 1,
35
+            'updated_by' => 1,
21 36
         ];
22 37
     }
38
+
39
+    public function withContacts(int $count = 1): self
40
+    {
41
+        return $this->has(Contact::factory()->count($count));
42
+    }
43
+
44
+    public function withPrimaryContact(): self
45
+    {
46
+        return $this->has(Contact::factory()->primary());
47
+    }
48
+
49
+    public function withAddresses(): self
50
+    {
51
+        return $this
52
+            ->has(Address::factory()->billing())
53
+            ->has(Address::factory()->shipping());
54
+    }
23 55
 }

+ 66
- 2
database/factories/Common/ContactFactory.php Прегледај датотеку

@@ -10,6 +10,11 @@ use Illuminate\Database\Eloquent\Factories\Factory;
10 10
  */
11 11
 class ContactFactory extends Factory
12 12
 {
13
+    /**
14
+     * The name of the factory's corresponding model.
15
+     */
16
+    protected $model = Contact::class;
17
+
13 18
     /**
14 19
      * Define the model's default state.
15 20
      *
@@ -18,8 +23,67 @@ class ContactFactory extends Factory
18 23
     public function definition(): array
19 24
     {
20 25
         return [
21
-            'name' => $this->faker->name(),
22
-            'email' => $this->faker->unique()->safeEmail(),
26
+            'company_id' => 1,
27
+            'first_name' => $this->faker->firstName,
28
+            'last_name' => $this->faker->lastName,
29
+            'email' => $this->faker->unique()->safeEmail,
30
+            'phones' => $this->generatePhones(),
31
+            'is_primary' => $this->faker->boolean(50),
32
+            'created_by' => 1,
33
+            'updated_by' => 1,
23 34
         ];
24 35
     }
36
+
37
+    protected function generatePhones(): array
38
+    {
39
+        $phones = [];
40
+
41
+        if ($this->faker->boolean(80)) {
42
+            $phones[] = [
43
+                'data' => ['number' => $this->faker->phoneNumber],
44
+                'type' => 'primary',
45
+            ];
46
+        }
47
+
48
+        if ($this->faker->boolean(50)) {
49
+            $phones[] = [
50
+                'data' => ['number' => $this->faker->phoneNumber],
51
+                'type' => 'mobile',
52
+            ];
53
+        }
54
+
55
+        if ($this->faker->boolean(30)) {
56
+            $phones[] = [
57
+                'data' => ['number' => $this->faker->phoneNumber],
58
+                'type' => 'toll_free',
59
+            ];
60
+        }
61
+
62
+        if ($this->faker->boolean(10)) {
63
+            $phones[] = [
64
+                'data' => ['number' => $this->faker->phoneNumber],
65
+                'type' => 'fax',
66
+            ];
67
+        }
68
+
69
+        return $phones;
70
+    }
71
+
72
+    public function primary(): self
73
+    {
74
+        return $this->state(function (array $attributes) {
75
+            return [
76
+                'is_primary' => true,
77
+            ];
78
+        });
79
+    }
80
+
81
+    public function secondary(): self
82
+    {
83
+        return $this->state(function (array $attributes) {
84
+            return [
85
+                'is_primary' => false,
86
+            ];
87
+        });
88
+    }
25 89
 }

+ 94
- 2
database/factories/Common/OfferingFactory.php Прегледај датотеку

@@ -2,13 +2,25 @@
2 2
 
3 3
 namespace Database\Factories\Common;
4 4
 
5
+use App\Enums\Accounting\AccountCategory;
6
+use App\Enums\Accounting\AccountType;
7
+use App\Enums\Accounting\AdjustmentType;
8
+use App\Enums\Common\OfferingType;
9
+use App\Models\Accounting\Account;
10
+use App\Models\Accounting\Adjustment;
11
+use App\Models\Common\Offering;
5 12
 use Illuminate\Database\Eloquent\Factories\Factory;
6 13
 
7 14
 /**
8
- * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Common\Offering>
15
+ * @extends Factory<Offering>
9 16
  */
10 17
 class OfferingFactory extends Factory
11 18
 {
19
+    /**
20
+     * The name of the factory's corresponding model.
21
+     */
22
+    protected $model = Offering::class;
23
+
12 24
     /**
13 25
      * Define the model's default state.
14 26
      *
@@ -17,7 +29,87 @@ class OfferingFactory extends Factory
17 29
     public function definition(): array
18 30
     {
19 31
         return [
20
-            //
32
+            'company_id' => 1,
33
+            'name' => $this->faker->words(3, true),
34
+            'description' => $this->faker->sentence,
35
+            'type' => $this->faker->randomElement(OfferingType::cases()),
36
+            'price' => $this->faker->numberBetween(5, 1000),
37
+            'sellable' => $this->faker->boolean(80),
38
+            'purchasable' => $this->faker->boolean(80),
39
+            'income_account_id' => function (array $attributes) {
40
+                return $attributes['sellable'] ? 10 : null;
41
+            },
42
+            'expense_account_id' => function (array $attributes) {
43
+                return $attributes['purchasable'] ? $this->faker->numberBetween(17, 35) : null;
44
+            },
45
+            'created_by' => 1,
46
+            'updated_by' => 1,
21 47
         ];
22 48
     }
49
+
50
+    public function sellable(): self
51
+    {
52
+        $incomeAccount = Account::query()
53
+            ->where('category', AccountCategory::Revenue)
54
+            ->where('type', AccountType::OperatingRevenue)
55
+            ->inRandomOrder()
56
+            ->first();
57
+
58
+        return $this->state(function (array $attributes) use ($incomeAccount) {
59
+            return [
60
+                'sellable' => true,
61
+                'income_account_id' => $incomeAccount?->id ?? 10,
62
+            ];
63
+        });
64
+    }
65
+
66
+    public function purchasable(): self
67
+    {
68
+        $expenseAccount = Account::query()
69
+            ->where('category', AccountCategory::Expense)
70
+            ->where('type', AccountType::OperatingExpense)
71
+            ->inRandomOrder()
72
+            ->first();
73
+
74
+        return $this->state(function (array $attributes) use ($expenseAccount) {
75
+            return [
76
+                'purchasable' => true,
77
+                'expense_account_id' => $expenseAccount?->id ?? $this->faker->numberBetween(17, 35),
78
+            ];
79
+        });
80
+    }
81
+
82
+    public function withSalesAdjustments(): self
83
+    {
84
+        return $this->afterCreating(function (Offering $offering) {
85
+            if ($offering->sellable) {
86
+                $adjustments = $offering->company?->adjustments()
87
+                    ->where('type', AdjustmentType::Sales)
88
+                    ->pluck('id');
89
+
90
+                $adjustmentsToAttach = $adjustments->isNotEmpty()
91
+                    ? $adjustments->random(min(2, $adjustments->count()))
92
+                    : Adjustment::factory()->salesTax()->count(2)->create()->pluck('id');
93
+
94
+                $offering->salesAdjustments()->attach($adjustmentsToAttach);
95
+            }
96
+        });
97
+    }
98
+
99
+    public function withPurchaseAdjustments(): self
100
+    {
101
+        return $this->afterCreating(function (Offering $offering) {
102
+            if ($offering->purchasable) {
103
+                $adjustments = $offering->company?->adjustments()
104
+                    ->where('type', AdjustmentType::Purchase)
105
+                    ->pluck('id');
106
+
107
+                $adjustmentsToAttach = $adjustments->isNotEmpty()
108
+                    ? $adjustments->random(min(2, $adjustments->count()))
109
+                    : Adjustment::factory()->purchaseTax()->count(2)->create()->pluck('id');
110
+
111
+                $offering->purchaseAdjustments()->attach($adjustmentsToAttach);
112
+            }
113
+        });
114
+    }
23 115
 }

+ 69
- 2
database/factories/Common/VendorFactory.php Прегледај датотеку

@@ -2,13 +2,23 @@
2 2
 
3 3
 namespace Database\Factories\Common;
4 4
 
5
+use App\Enums\Common\ContractorType;
6
+use App\Enums\Common\VendorType;
7
+use App\Models\Common\Address;
8
+use App\Models\Common\Contact;
9
+use App\Models\Common\Vendor;
5 10
 use Illuminate\Database\Eloquent\Factories\Factory;
6 11
 
7 12
 /**
8
- * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Common\Vendor>
13
+ * @extends Factory<Vendor>
9 14
  */
10 15
 class VendorFactory extends Factory
11 16
 {
17
+    /**
18
+     * The name of the factory's corresponding model.
19
+     */
20
+    protected $model = Vendor::class;
21
+
12 22
     /**
13 23
      * Define the model's default state.
14 24
      *
@@ -17,7 +27,64 @@ class VendorFactory extends Factory
17 27
     public function definition(): array
18 28
     {
19 29
         return [
20
-            //
30
+            'company_id' => 1,
31
+            'name' => $this->faker->company,
32
+            'type' => $this->faker->randomElement(VendorType::cases()),
33
+            'contractor_type' => function (array $attributes) {
34
+                return $attributes['type'] === VendorType::Contractor ? $this->faker->randomElement(ContractorType::cases()) : null;
35
+            },
36
+            'ssn' => function (array $attributes) {
37
+                return $attributes['contractor_type'] === ContractorType::Individual ? $this->faker->numerify(str_repeat('#', 9)) : null;
38
+            },
39
+            'ein' => function (array $attributes) {
40
+                return $attributes['contractor_type'] === ContractorType::Business ? $this->faker->numerify(str_repeat('#', 9)) : null;
41
+            },
42
+            'currency_code' => 'USD',
43
+            'account_number' => $this->faker->unique()->numerify(str_repeat('#', 12)),
44
+            'website' => $this->faker->url,
45
+            'notes' => $this->faker->sentence,
46
+            'created_by' => 1,
47
+            'updated_by' => 1,
21 48
         ];
22 49
     }
50
+
51
+    public function regular(): self
52
+    {
53
+        return $this->state([
54
+            'type' => VendorType::Regular,
55
+        ]);
56
+    }
57
+
58
+    public function contractor(): self
59
+    {
60
+        return $this->state([
61
+            'type' => VendorType::Contractor,
62
+        ]);
63
+    }
64
+
65
+    public function individualContractor(): self
66
+    {
67
+        return $this->state([
68
+            'type' => VendorType::Contractor,
69
+            'contractor_type' => ContractorType::Individual,
70
+        ]);
71
+    }
72
+
73
+    public function businessContractor(): self
74
+    {
75
+        return $this->state([
76
+            'type' => VendorType::Contractor,
77
+            'contractor_type' => ContractorType::Business,
78
+        ]);
79
+    }
80
+
81
+    public function withContact(): self
82
+    {
83
+        return $this->has(Contact::factory()->primary());
84
+    }
85
+
86
+    public function withAddress(): self
87
+    {
88
+        return $this->has(Address::factory()->general());
89
+    }
23 90
 }

+ 30
- 0
database/factories/CompanyFactory.php Прегледај датотеку

@@ -3,6 +3,9 @@
3 3
 namespace Database\Factories;
4 4
 
5 5
 use App\Models\Accounting\Transaction;
6
+use App\Models\Common\Client;
7
+use App\Models\Common\Offering;
8
+use App\Models\Common\Vendor;
6 9
 use App\Models\Company;
7 10
 use App\Models\Setting\CompanyProfile;
8 11
 use App\Models\User;
@@ -65,4 +68,31 @@ class CompanyFactory extends Factory
65 68
                 ]);
66 69
         });
67 70
     }
71
+
72
+    public function withClients(int $count = 10): self
73
+    {
74
+        return $this->has(Client::factory()->count($count)->withPrimaryContact()->withAddresses());
75
+    }
76
+
77
+    public function withVendors(int $count = 10): self
78
+    {
79
+        return $this->has(Vendor::factory()->count($count)->withContact()->withAddress());
80
+    }
81
+
82
+    public function withOfferings(int $count = 10): self
83
+    {
84
+        return $this->afterCreating(function (Company $company) use ($count) {
85
+            Offering::factory()
86
+                ->count($count)
87
+                ->sellable()
88
+                ->withSalesAdjustments()
89
+                ->purchasable()
90
+                ->withPurchaseAdjustments()
91
+                ->create([
92
+                    'company_id' => $company->id,
93
+                    'created_by' => $company->user_id,
94
+                    'updated_by' => $company->user_id,
95
+                ]);
96
+        });
97
+    }
68 98
 }

+ 1
- 1
database/migrations/2024_11_13_215818_create_payments_table.php Прегледај датотеку

@@ -14,7 +14,7 @@ return new class extends Migration
14 14
         Schema::create('payments', function (Blueprint $table) {
15 15
             $table->id();
16 16
             $table->foreignId('company_id')->constrained()->cascadeOnDelete();
17
-            $table->foreignId('document_id')->constrained()->cascadeOnDelete();
17
+            $table->morphs('payable');
18 18
             $table->date('date');
19 19
             $table->integer('amount');
20 20
             $table->string('payment_method');

+ 1
- 1
database/migrations/2024_11_13_220301_create_document_line_items_table.php Прегледај датотеку

@@ -14,7 +14,7 @@ return new class extends Migration
14 14
         Schema::create('document_line_items', function (Blueprint $table) {
15 15
             $table->id();
16 16
             $table->foreignId('company_id')->constrained()->cascadeOnDelete();
17
-            $table->foreignId('document_id')->constrained()->cascadeOnDelete();
17
+            $table->morphs('documentable');
18 18
             $table->foreignId('offering_id')->nullable()->constrained()->nullOnDelete();
19 19
             $table->string('description')->nullable();
20 20
             $table->integer('quantity')->default(1);

+ 43
- 0
database/migrations/2024_11_27_221657_create_bills_table.php Прегледај датотеку

@@ -0,0 +1,43 @@
1
+<?php
2
+
3
+use Illuminate\Database\Migrations\Migration;
4
+use Illuminate\Database\Schema\Blueprint;
5
+use Illuminate\Support\Facades\Schema;
6
+
7
+return new class extends Migration
8
+{
9
+    /**
10
+     * Run the migrations.
11
+     */
12
+    public function up(): void
13
+    {
14
+        Schema::create('bills', function (Blueprint $table) {
15
+            $table->id();
16
+            $table->foreignId('company_id')->constrained()->cascadeOnDelete();
17
+            $table->foreignId('vendor_id')->nullable()->constrained('vendors')->nullOnDelete();
18
+            $table->string('bill_number')->nullable();
19
+            $table->string('order_number')->nullable(); // PO, SO, etc.
20
+            $table->date('date')->nullable();
21
+            $table->date('due_date')->nullable();
22
+            $table->string('status')->default('draft');
23
+            $table->string('currency_code')->nullable();
24
+            $table->integer('subtotal')->default(0);
25
+            $table->integer('tax_total')->default(0);
26
+            $table->integer('discount_total')->default(0);
27
+            $table->integer('total')->default(0);
28
+            $table->integer('amount_paid')->default(0);
29
+            $table->integer('amount_due')->storedAs('total - amount_paid');
30
+            $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
31
+            $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
32
+            $table->timestamps();
33
+        });
34
+    }
35
+
36
+    /**
37
+     * Reverse the migrations.
38
+     */
39
+    public function down(): void
40
+    {
41
+        Schema::dropIfExists('bills');
42
+    }
43
+};

database/migrations/2024_11_13_214900_create_documents_table.php → database/migrations/2024_11_27_223015_create_invoices_table.php Прегледај датотеку

@@ -11,16 +11,14 @@ return new class extends Migration
11 11
      */
12 12
     public function up(): void
13 13
     {
14
-        Schema::create('documents', function (Blueprint $table) {
14
+        Schema::create('invoices', function (Blueprint $table) {
15 15
             $table->id();
16 16
             $table->foreignId('company_id')->constrained()->cascadeOnDelete();
17 17
             $table->foreignId('client_id')->nullable()->constrained('clients')->nullOnDelete();
18
-            $table->foreignId('vendor_id')->nullable()->constrained('vendors')->nullOnDelete();
19
-            $table->string('type'); // invoice, bill, etc.
20 18
             $table->string('logo')->nullable();
21 19
             $table->string('header')->nullable();
22 20
             $table->string('subheader')->nullable();
23
-            $table->string('document_number')->nullable();
21
+            $table->string('invoice_number')->nullable();
24 22
             $table->string('order_number')->nullable(); // PO, SO, etc.
25 23
             $table->date('date')->nullable();
26 24
             $table->date('due_date')->nullable();
@@ -45,6 +43,6 @@ return new class extends Migration
45 43
      */
46 44
     public function down(): void
47 45
     {
48
-        Schema::dropIfExists('documents');
46
+        Schema::dropIfExists('invoices');
49 47
     }
50 48
 };

+ 4
- 1
database/seeders/DatabaseSeeder.php Прегледај датотеку

@@ -20,7 +20,10 @@ class DatabaseSeeder extends Seeder
20 20
                     ->state([
21 21
                         'name' => 'ERPSAAS',
22 22
                     ])
23
-                    ->withTransactions();
23
+                    ->withTransactions()
24
+                    ->withOfferings()
25
+                    ->withClients()
26
+                    ->withVendors();
24 27
             })
25 28
             ->create([
26 29
                 'name' => 'Admin',

Loading…
Откажи
Сачувај