Andrew Wallo hace 10 meses
padre
commit
7d4ea3fa4b

+ 9
- 0
app/Enums/Accounting/DocumentType.php Ver fichero

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

+ 204
- 27
app/Filament/Company/Resources/Accounting/DocumentResource.php Ver fichero

@@ -2,9 +2,12 @@
2 2
 
3 3
 namespace App\Filament\Company\Resources\Accounting;
4 4
 
5
+use App\Enums\Accounting\AdjustmentCategory;
5 6
 use App\Filament\Company\Resources\Accounting\DocumentResource\Pages;
7
+use App\Models\Accounting\Adjustment;
6 8
 use App\Models\Accounting\Document;
7 9
 use App\Models\Common\Offering;
10
+use App\Utilities\Currency\CurrencyAccessor;
8 11
 use Awcodes\TableRepeater\Components\TableRepeater;
9 12
 use Awcodes\TableRepeater\Header;
10 13
 use Filament\Forms;
@@ -77,7 +80,10 @@ class DocumentResource extends Resource
77 80
                         Forms\Components\Split::make([
78 81
                             Forms\Components\Group::make([
79 82
                                 Forms\Components\Select::make('client_id')
80
-                                    ->relationship('client', 'name'),
83
+                                    ->relationship('client', 'name')
84
+                                    ->preload()
85
+                                    ->searchable()
86
+                                    ->required(),
81 87
                             ]),
82 88
                             Forms\Components\Group::make([
83 89
                                 Forms\Components\TextInput::make('document_number')
@@ -98,51 +104,225 @@ class DocumentResource extends Resource
98 104
                         TableRepeater::make('lineItems')
99 105
                             ->relationship()
100 106
                             ->headers([
101
-                                Header::make('Items'),
102
-                                Header::make('Description'),
103
-                                Header::make('Quantity'),
104
-                                Header::make('Price'),
105
-                                Header::make('Amount'),
107
+                                Header::make('Items')->width('20%'),
108
+                                Header::make('Description')->width('30%'),
109
+                                Header::make('Quantity')->width('10%'),
110
+                                Header::make('Price')->width('10%'),
111
+                                Header::make('Taxes')->width('20%'),
112
+                                Header::make('Amount')->width('10%')->align('right'),
106 113
                             ])
114
+                            ->live()
107 115
                             ->schema([
108 116
                                 Forms\Components\Select::make('offering_id')
109 117
                                     ->relationship('offering', 'name')
110 118
                                     ->preload()
111 119
                                     ->searchable()
112 120
                                     ->required()
113
-                                    ->live()
114
-                                    ->afterStateUpdated(function (Forms\Set $set, $state) {
121
+                                    ->afterStateUpdated(function (Forms\Set $set, Forms\Get $get, $state) {
115 122
                                         $offeringId = $state;
116
-                                        $offeringRecord = Offering::find($offeringId);
117
-                                        $set('description', $offeringRecord->description);
118
-                                        $set('unit_price', $offeringRecord->price);
119
-                                        $set('total', $offeringRecord->price);
123
+                                        $offeringRecord = Offering::with('salesTaxes')->find($offeringId);
124
+
125
+                                        if ($offeringRecord) {
126
+                                            $set('description', $offeringRecord->description);
127
+                                            $set('unit_price', $offeringRecord->price);
128
+                                            $set('salesTaxes', $offeringRecord->salesTaxes->pluck('id')->toArray());
129
+
130
+                                            $quantity = $get('quantity');
131
+                                            $total = $quantity * $offeringRecord->price;
132
+
133
+                                            // Calculate taxes and update total
134
+                                            $taxAmount = $offeringRecord->salesTaxes->sum(fn ($tax) => $total * ($tax->rate / 100));
135
+                                            $set('total', $total + $taxAmount);
136
+                                        }
120 137
                                     }),
121 138
                                 Forms\Components\TextInput::make('description')
122 139
                                     ->required(),
123 140
                                 Forms\Components\TextInput::make('quantity')
124 141
                                     ->required()
125 142
                                     ->numeric()
126
-                                    ->live()
127 143
                                     ->default(1),
128
-                                Forms\Components\Group::make([
129
-                                    Forms\Components\TextInput::make('unit_price')
130
-                                        ->hiddenLabel()
131
-                                        ->live()
132
-                                        ->numeric()
133
-                                        ->default(0),
134
-                                ]),
144
+                                Forms\Components\TextInput::make('unit_price')
145
+                                    ->hiddenLabel()
146
+                                    ->numeric()
147
+                                    ->default(0),
148
+                                Forms\Components\Select::make('salesTaxes')
149
+                                    ->relationship('salesTaxes', 'name')
150
+                                    ->preload()
151
+                                    ->multiple()
152
+                                    ->searchable(),
135 153
                                 Forms\Components\Placeholder::make('total')
136 154
                                     ->hiddenLabel()
137 155
                                     ->content(function (Forms\Get $get) {
138
-                                        $quantity = $get('quantity');
139
-                                        $unitPrice = $get('unit_price');
156
+                                        $quantity = $get('quantity') ?? 0;
157
+                                        $unitPrice = $get('unit_price') ?? 0;
158
+                                        $salesTaxes = $get('salesTaxes') ?? [];
159
+
160
+                                        $total = $quantity * $unitPrice;
161
+
162
+                                        if (! empty($salesTaxes)) {
163
+                                            $taxRates = Adjustment::whereIn('id', $salesTaxes)->pluck('rate');
140 164
 
141
-                                        if ($quantity && $unitPrice) {
142
-                                            return $quantity * $unitPrice;
165
+                                            $taxAmount = $taxRates->sum(function ($rate) use ($total) {
166
+                                                return $total * ($rate / 100);
167
+                                            });
168
+
169
+                                            $total += $taxAmount;
170
+
171
+                                            return money($total, CurrencyAccessor::getDefaultCurrency(), true)->format();
143 172
                                         }
173
+
174
+                                        return money($total, CurrencyAccessor::getDefaultCurrency(), true)->format();
144 175
                                     }),
145 176
                             ]),
177
+                        Forms\Components\Grid::make(6)
178
+                            ->inlineLabel()
179
+                            ->extraAttributes([
180
+                                'class' => 'text-right pr-16',
181
+                            ])
182
+                            ->schema([
183
+                                Forms\Components\Group::make([
184
+                                    Forms\Components\Placeholder::make('subtotal')
185
+                                        ->label('Subtotal')
186
+                                        ->content(function (Forms\Get $get) {
187
+                                            $lineItems = $get('lineItems');
188
+
189
+                                            $subtotal = collect($lineItems)
190
+                                                ->sum(fn ($item) => ($item['quantity'] ?? 0) * ($item['unit_price'] ?? 0));
191
+
192
+                                            return money($subtotal, CurrencyAccessor::getDefaultCurrency(), true)->format();
193
+                                        }),
194
+                                    Forms\Components\Placeholder::make('tax_total')
195
+                                        ->label('Taxes')
196
+                                        ->content(function (Forms\Get $get) {
197
+                                            $lineItems = $get('lineItems');
198
+
199
+                                            $totalTaxes = collect($lineItems)->reduce(function ($carry, $item) {
200
+                                                $quantity = $item['quantity'] ?? 0;
201
+                                                $unitPrice = $item['unit_price'] ?? 0;
202
+                                                $salesTaxes = $item['salesTaxes'] ?? [];
203
+                                                $lineTotal = $quantity * $unitPrice;
204
+
205
+                                                $taxAmount = Adjustment::whereIn('id', $salesTaxes)
206
+                                                    ->pluck('rate')
207
+                                                    ->sum(fn ($rate) => $lineTotal * ($rate / 100));
208
+
209
+                                                return $carry + $taxAmount;
210
+                                            }, 0);
211
+
212
+                                            return money($totalTaxes, CurrencyAccessor::getDefaultCurrency(), true)->format();
213
+                                        }),
214
+                                    Forms\Components\Placeholder::make('total')
215
+                                        ->label('Total')
216
+                                        ->content(function (Forms\Get $get) {
217
+                                            $lineItems = $get('lineItems') ?? [];
218
+
219
+                                            $subtotal = collect($lineItems)
220
+                                                ->sum(fn ($item) => ($item['quantity'] ?? 0) * ($item['unit_price'] ?? 0));
221
+
222
+                                            $totalTaxes = collect($lineItems)->reduce(function ($carry, $item) {
223
+                                                $quantity = $item['quantity'] ?? 0;
224
+                                                $unitPrice = $item['unit_price'] ?? 0;
225
+                                                $salesTaxes = $item['salesTaxes'] ?? [];
226
+                                                $lineTotal = $quantity * $unitPrice;
227
+
228
+                                                $taxAmount = Adjustment::whereIn('id', $salesTaxes)
229
+                                                    ->pluck('rate')
230
+                                                    ->sum(fn ($rate) => $lineTotal * ($rate / 100));
231
+
232
+                                                return $carry + $taxAmount;
233
+                                            }, 0);
234
+
235
+                                            $grandTotal = $subtotal + $totalTaxes;
236
+
237
+                                            return money($grandTotal, CurrencyAccessor::getDefaultCurrency(), true)->format();
238
+                                        }),
239
+                                ])->columnStart(6),
240
+                            ]),
241
+                        //                        Forms\Components\Repeater::make('lineItems')
242
+                        //                            ->relationship()
243
+                        //                            ->columns(8)
244
+                        //                            ->schema([
245
+                        //                                Forms\Components\Select::make('offering_id')
246
+                        //                                    ->relationship('offering', 'name')
247
+                        //                                    ->preload()
248
+                        //                                    ->columnSpan(2)
249
+                        //                                    ->searchable()
250
+                        //                                    ->required()
251
+                        //                                    ->live()
252
+                        //                                    ->afterStateUpdated(function (Forms\Set $set, $state) {
253
+                        //                                        $offeringId = $state;
254
+                        //                                        $offeringRecord = Offering::with('salesTaxes')->find($offeringId);
255
+                        //
256
+                        //                                        if ($offeringRecord) {
257
+                        //                                            $set('description', $offeringRecord->description);
258
+                        //                                            $set('unit_price', $offeringRecord->price);
259
+                        //                                            $set('total', $offeringRecord->price);
260
+                        //
261
+                        //                                            $salesTaxes = $offeringRecord->salesTaxes->map(function ($tax) {
262
+                        //                                                return [
263
+                        //                                                    'id' => $tax->id,
264
+                        //                                                    'amount' => null, // Amount will be calculated dynamically
265
+                        //                                                ];
266
+                        //                                            })->toArray();
267
+                        //
268
+                        //                                            $set('taxes', $salesTaxes);
269
+                        //                                        }
270
+                        //                                    }),
271
+                        //                                Forms\Components\TextInput::make('description')
272
+                        //                                    ->columnSpan(3)
273
+                        //                                    ->required(),
274
+                        //                                Forms\Components\TextInput::make('quantity')
275
+                        //                                    ->required()
276
+                        //                                    ->numeric()
277
+                        //                                    ->live()
278
+                        //                                    ->default(1),
279
+                        //                                Forms\Components\TextInput::make('unit_price')
280
+                        //                                    ->live()
281
+                        //                                    ->numeric()
282
+                        //                                    ->default(0),
283
+                        //                                Forms\Components\Placeholder::make('total')
284
+                        //                                    ->content(function (Forms\Get $get) {
285
+                        //                                        $quantity = $get('quantity');
286
+                        //                                        $unitPrice = $get('unit_price');
287
+                        //
288
+                        //                                        if ($quantity && $unitPrice) {
289
+                        //                                            return $quantity * $unitPrice;
290
+                        //                                        }
291
+                        //                                    }),
292
+                        //                                TableRepeater::make('taxes')
293
+                        //                                    ->relationship()
294
+                        //                                    ->columnSpanFull()
295
+                        //                                    ->columnStart(6)
296
+                        //                                    ->headers([
297
+                        //                                        Header::make('')->width('200px'),
298
+                        //                                        Header::make('')->width('50px')->align('right'),
299
+                        //                                    ])
300
+                        //                                    ->defaultItems(0)
301
+                        //                                    ->schema([
302
+                        //                                        Forms\Components\Select::make('id') // The ID of the adjustment being attached.
303
+                        //                                            ->label('Tax Adjustment')
304
+                        //                                            ->options(
305
+                        //                                                Adjustment::query()
306
+                        //                                                    ->where('category', AdjustmentCategory::Tax)
307
+                        //                                                    ->pluck('name', 'id')
308
+                        //                                            )
309
+                        //                                            ->preload()
310
+                        //                                            ->searchable()
311
+                        //                                            ->required()
312
+                        //                                            ->live(),
313
+                        //                                        Forms\Components\Placeholder::make('amount')
314
+                        //                                            ->hiddenLabel()
315
+                        //                                            ->content(function (Forms\Get $get) {
316
+                        //                                                $quantity = $get('../../quantity') ?? 0; // Get parent quantity
317
+                        //                                                $unitPrice = $get('../../unit_price') ?? 0; // Get parent unit price
318
+                        //                                                $rate = Adjustment::find($get('id'))->rate ?? 0;
319
+                        //
320
+                        //                                                $total = $quantity * $unitPrice;
321
+                        //
322
+                        //                                                return $total * ($rate / 100);
323
+                        //                                            }),
324
+                        //                                    ]),
325
+                        //                            ]),
146 326
                         Forms\Components\Textarea::make('terms')
147 327
                             ->columnSpanFull(),
148 328
                     ]),
@@ -159,9 +339,6 @@ class DocumentResource extends Resource
159 339
     {
160 340
         return $table
161 341
             ->columns([
162
-                Tables\Columns\TextColumn::make('company.name')
163
-                    ->numeric()
164
-                    ->sortable(),
165 342
                 Tables\Columns\TextColumn::make('client.name')
166 343
                     ->numeric()
167 344
                     ->sortable(),

+ 6
- 0
app/Filament/Company/Resources/Accounting/DocumentResource/Pages/CreateDocument.php Ver fichero

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

+ 55
- 0
app/Filament/Company/Resources/Sales/InvoiceResource.php Ver fichero

@@ -0,0 +1,55 @@
1
+<?php
2
+
3
+namespace App\Filament\Company\Resources\Sales;
4
+
5
+use App\Enums\Accounting\DocumentType;
6
+use App\Filament\Company\Resources\Accounting\DocumentResource;
7
+use App\Filament\Company\Resources\Sales\InvoiceResource\Pages;
8
+use App\Models\Accounting\Document;
9
+use Filament\Forms\Form;
10
+use Filament\Resources\Resource;
11
+use Filament\Tables\Table;
12
+use Illuminate\Database\Eloquent\Builder;
13
+
14
+class InvoiceResource extends Resource
15
+{
16
+    protected static ?string $model = Document::class;
17
+
18
+    protected static ?string $navigationIcon = 'heroicon-o-rectangle-stack';
19
+
20
+    protected static ?string $pluralModelLabel = 'Invoices';
21
+
22
+    protected static ?string $modelLabel = 'Invoice';
23
+
24
+    public static function getEloquentQuery(): Builder
25
+    {
26
+        return parent::getEloquentQuery()
27
+            ->where('type', DocumentType::Invoice);
28
+    }
29
+
30
+    public static function form(Form $form): Form
31
+    {
32
+        return DocumentResource::form($form);
33
+    }
34
+
35
+    public static function table(Table $table): Table
36
+    {
37
+        return DocumentResource::table($table);
38
+    }
39
+
40
+    public static function getRelations(): array
41
+    {
42
+        return [
43
+            //
44
+        ];
45
+    }
46
+
47
+    public static function getPages(): array
48
+    {
49
+        return [
50
+            'index' => Pages\ListInvoices::route('/'),
51
+            'create' => Pages\CreateInvoice::route('/create'),
52
+            'edit' => Pages\EditInvoice::route('/{record}/edit'),
53
+        ];
54
+    }
55
+}

+ 24
- 0
app/Filament/Company/Resources/Sales/InvoiceResource/Pages/CreateInvoice.php Ver fichero

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

+ 19
- 0
app/Filament/Company/Resources/Sales/InvoiceResource/Pages/EditInvoice.php Ver fichero

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

+ 19
- 0
app/Filament/Company/Resources/Sales/InvoiceResource/Pages/ListInvoices.php Ver fichero

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

+ 165
- 0
app/Filament/Forms/Components/LineItemRepeater.php Ver fichero

@@ -0,0 +1,165 @@
1
+<?php
2
+
3
+namespace App\Filament\Forms\Components;
4
+
5
+use Awcodes\TableRepeater\Components\TableRepeater;
6
+use Closure;
7
+use Filament\Forms\ComponentContainer;
8
+use Filament\Forms\Components\Component;
9
+
10
+class LineItemRepeater extends TableRepeater
11
+{
12
+    protected array | Closure $nestedSchema = [];
13
+
14
+    protected ?string $nestedColumn = null;
15
+
16
+    /**
17
+     * Set nested schema and optionally the column it belongs to.
18
+     *
19
+     * @param  array<Component> | Closure  $components
20
+     */
21
+    public function withNestedSchema(array | Closure $components, ?string $underColumn = null): static
22
+    {
23
+        $this->nestedSchema = $components;
24
+        $this->nestedColumn = $underColumn;
25
+
26
+        return $this;
27
+    }
28
+
29
+    /**
30
+     * Get the nested schema.
31
+     *
32
+     * @return array<Component>
33
+     */
34
+    public function getNestedSchema(): array
35
+    {
36
+        return $this->evaluate($this->nestedSchema);
37
+    }
38
+
39
+    /**
40
+     * Get the column under which the nested schema should be rendered.
41
+     */
42
+    public function getNestedColumn(): ?string
43
+    {
44
+        return $this->nestedColumn;
45
+    }
46
+
47
+    /**
48
+     * Determine if there is a nested schema defined.
49
+     */
50
+    public function hasNestedSchema(): bool
51
+    {
52
+        return ! empty($this->getNestedSchema());
53
+    }
54
+
55
+    public function getNestedSchemaMap(): array
56
+    {
57
+        return collect($this->getNestedSchema())
58
+            ->keyBy(fn ($component) => $component->getKey())
59
+            ->all();
60
+    }
61
+
62
+    /**
63
+     * Get all child components, including nested schema.
64
+     *
65
+     * @return array<Component>
66
+     */
67
+    public function getChildComponents(): array
68
+    {
69
+        $components = parent::getChildComponents();
70
+
71
+        if ($this->hasNestedSchema()) {
72
+            $components = array_merge($components, $this->getNestedSchema());
73
+        }
74
+
75
+        return $components;
76
+    }
77
+
78
+    public function getChildComponentContainers(bool $withHidden = false): array
79
+    {
80
+        if ((! $withHidden) && $this->isHidden()) {
81
+            return [];
82
+        }
83
+
84
+        $relationship = $this->getRelationship();
85
+
86
+        $records = $relationship ? $this->getCachedExistingRecords() : null;
87
+
88
+        $containers = [];
89
+
90
+        foreach ($this->getState() ?? [] as $itemKey => $itemData) {
91
+            $containers[$itemKey] = $this
92
+                ->getChildComponentContainer()
93
+                ->statePath($itemKey)
94
+                ->model($relationship ? $records[$itemKey] ?? $this->getRelatedModel() : null)
95
+                ->inlineLabel(false)
96
+                ->getClone();
97
+        }
98
+
99
+        return $containers;
100
+    }
101
+
102
+    public function getChildComponentContainersWithoutNestedSchema(bool $withHidden = false): array
103
+    {
104
+        if ((! $withHidden) && $this->isHidden()) {
105
+            return [];
106
+        }
107
+
108
+        $relationship = $this->getRelationship();
109
+        $records = $relationship ? $this->getCachedExistingRecords() : null;
110
+
111
+        $containers = [];
112
+
113
+        $childComponentsWithoutNestedSchema = $this->getChildComponentsWithoutNestedSchema();
114
+
115
+        foreach ($this->getState() ?? [] as $itemKey => $itemData) {
116
+            $containers[$itemKey] = ComponentContainer::make($this->getLivewire())
117
+                ->parentComponent($this)
118
+                ->statePath($itemKey)
119
+                ->model($relationship ? $records[$itemKey] ?? $this->getRelatedModel() : null)
120
+                ->components($childComponentsWithoutNestedSchema)
121
+                ->inlineLabel(false)
122
+                ->getClone();
123
+        }
124
+
125
+        return $containers;
126
+    }
127
+
128
+    public function getChildComponentContainer($key = null): ComponentContainer
129
+    {
130
+        if (filled($key) && array_key_exists($key, $containers = $this->getChildComponentContainers())) {
131
+            return $containers[$key];
132
+        }
133
+
134
+        return ComponentContainer::make($this->getLivewire())
135
+            ->parentComponent($this)
136
+            ->components($this->getChildComponents());
137
+    }
138
+
139
+    public function getChildComponentsWithoutNestedSchema(): array
140
+    {
141
+        // Fetch the nested schema components.
142
+        $nestedSchema = $this->getNestedSchema();
143
+
144
+        // Filter out the nested schema components.
145
+        return array_filter($this->getChildComponents(), function ($component) use ($nestedSchema) {
146
+            return ! in_array($component, $nestedSchema, true);
147
+        });
148
+    }
149
+
150
+    public function getNestedComponents(): array
151
+    {
152
+        // Fetch the nested schema components.
153
+        $nestedSchema = $this->getNestedSchema();
154
+
155
+        // Separate and return only the nested schema components.
156
+        return array_filter($this->getChildComponents(), function ($component) use ($nestedSchema) {
157
+            return in_array($component, $nestedSchema, true);
158
+        });
159
+    }
160
+
161
+    public function getView(): string
162
+    {
163
+        return 'filament.forms.components.line-item-repeater';
164
+    }
165
+}

+ 0
- 15
app/Forms/Components/CustomSection.php Ver fichero

@@ -1,15 +0,0 @@
1
-<?php
2
-
3
-namespace App\Forms\Components;
4
-
5
-use Filament\Forms\Components\Component;
6
-
7
-class CustomSection extends Component
8
-{
9
-    protected string $view = 'forms.components.custom-section';
10
-
11
-    public static function make(): static
12
-    {
13
-        return app(static::class);
14
-    }
15
-}

+ 2
- 0
app/Models/Accounting/Document.php Ver fichero

@@ -5,6 +5,7 @@ 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\DocumentType;
8 9
 use App\Models\Banking\Payment;
9 10
 use App\Models\Common\Client;
10 11
 use App\Models\Common\Vendor;
@@ -47,6 +48,7 @@ class Document extends Model
47 48
     ];
48 49
 
49 50
     protected $casts = [
51
+        'type' => DocumentType::class,
50 52
         'date' => 'date',
51 53
         'due_date' => 'date',
52 54
         'subtotal' => MoneyCast::class,

+ 11
- 0
app/Models/Accounting/DocumentLineItem.php Ver fichero

@@ -6,6 +6,7 @@ use App\Casts\MoneyCast;
6 6
 use App\Concerns\Blamable;
7 7
 use App\Concerns\CompanyOwned;
8 8
 use App\Enums\Accounting\AdjustmentCategory;
9
+use App\Enums\Accounting\AdjustmentType;
9 10
 use App\Models\Common\Offering;
10 11
 use Illuminate\Database\Eloquent\Factories\HasFactory;
11 12
 use Illuminate\Database\Eloquent\Model;
@@ -56,6 +57,16 @@ class DocumentLineItem extends Model
56 57
         return $this->morphToMany(Adjustment::class, 'adjustmentable', 'adjustmentables');
57 58
     }
58 59
 
60
+    public function salesTaxes(): MorphToMany
61
+    {
62
+        return $this->adjustments()->where('category', AdjustmentCategory::Tax)->where('type', AdjustmentType::Sales);
63
+    }
64
+
65
+    public function salesDiscounts(): MorphToMany
66
+    {
67
+        return $this->adjustments()->where('category', AdjustmentCategory::Discount)->where('type', AdjustmentType::Sales);
68
+    }
69
+
59 70
     public function taxes(): MorphToMany
60 71
     {
61 72
         return $this->adjustments()->where('category', AdjustmentCategory::Tax);

+ 9
- 0
app/Models/Common/Client.php Ver fichero

@@ -4,11 +4,14 @@ namespace App\Models\Common;
4 4
 
5 5
 use App\Concerns\Blamable;
6 6
 use App\Concerns\CompanyOwned;
7
+use App\Enums\Accounting\DocumentType;
7 8
 use App\Enums\Common\AddressType;
9
+use App\Models\Accounting\Document;
8 10
 use App\Models\Setting\Currency;
9 11
 use Illuminate\Database\Eloquent\Factories\HasFactory;
10 12
 use Illuminate\Database\Eloquent\Model;
11 13
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
14
+use Illuminate\Database\Eloquent\Relations\HasMany;
12 15
 use Illuminate\Database\Eloquent\Relations\MorphMany;
13 16
 use Illuminate\Database\Eloquent\Relations\MorphOne;
14 17
 
@@ -69,4 +72,10 @@ class Client extends Model
69 72
         return $this->morphOne(Address::class, 'addressable')
70 73
             ->where('type', AddressType::Shipping);
71 74
     }
75
+
76
+    public function invoices(): HasMany
77
+    {
78
+        return $this->hasMany(Document::class, 'client_id')
79
+            ->where('type', DocumentType::Invoice);
80
+    }
72 81
 }

+ 9
- 0
app/Models/Common/Vendor.php Ver fichero

@@ -4,12 +4,15 @@ namespace App\Models\Common;
4 4
 
5 5
 use App\Concerns\Blamable;
6 6
 use App\Concerns\CompanyOwned;
7
+use App\Enums\Accounting\DocumentType;
7 8
 use App\Enums\Common\ContractorType;
8 9
 use App\Enums\Common\VendorType;
10
+use App\Models\Accounting\Document;
9 11
 use App\Models\Setting\Currency;
10 12
 use Illuminate\Database\Eloquent\Factories\HasFactory;
11 13
 use Illuminate\Database\Eloquent\Model;
12 14
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
15
+use Illuminate\Database\Eloquent\Relations\HasMany;
13 16
 use Illuminate\Database\Eloquent\Relations\MorphOne;
14 17
 
15 18
 class Vendor extends Model
@@ -42,6 +45,12 @@ class Vendor extends Model
42 45
         'ein' => 'encrypted',
43 46
     ];
44 47
 
48
+    public function bills(): HasMany
49
+    {
50
+        return $this->hasMany(Document::class, 'vendor_id')
51
+            ->where('type', DocumentType::Bill);
52
+    }
53
+
45 54
     public function currency(): BelongsTo
46 55
     {
47 56
         return $this->belongsTo(Currency::class, 'currency_code', 'code');

+ 5
- 0
app/Models/Company.php Ver fichero

@@ -137,6 +137,11 @@ class Company extends FilamentCompaniesCompany implements HasAvatar
137 137
         return $this->hasMany(Department::class, 'company_id');
138 138
     }
139 139
 
140
+    public function documents(): HasMany
141
+    {
142
+        return $this->hasMany(Accounting\Document::class, 'company_id');
143
+    }
144
+
140 145
     public function locale(): HasOne
141 146
     {
142 147
         return $this->hasOne(Localization::class, 'company_id');

+ 5
- 1
app/Providers/FilamentCompaniesServiceProvider.php Ver fichero

@@ -30,6 +30,7 @@ use App\Filament\Company\Resources\Banking\AccountResource;
30 30
 use App\Filament\Company\Resources\Common\ClientResource;
31 31
 use App\Filament\Company\Resources\Common\OfferingResource;
32 32
 use App\Filament\Company\Resources\Common\VendorResource;
33
+use App\Filament\Company\Resources\Sales\InvoiceResource;
33 34
 use App\Filament\Components\PanelShiftDropdown;
34 35
 use App\Filament\User\Clusters\Account;
35 36
 use App\Http\Middleware\ConfigureCurrentCompany;
@@ -127,7 +128,10 @@ class FilamentCompaniesServiceProvider extends PanelProvider
127 128
                         NavigationGroup::make('Sales & Payments')
128 129
                             ->label('Sales & Payments')
129 130
                             ->icon('heroicon-o-currency-dollar')
130
-                            ->items(ClientResource::getNavigationItems()),
131
+                            ->items([
132
+                                ...InvoiceResource::getNavigationItems(),
133
+                                ...ClientResource::getNavigationItems(),
134
+                            ]),
131 135
                         NavigationGroup::make('Purchases')
132 136
                             ->label('Purchases')
133 137
                             ->icon('heroicon-o-shopping-cart')

+ 6
- 6
composer.lock Ver fichero

@@ -2096,16 +2096,16 @@
2096 2096
         },
2097 2097
         {
2098 2098
             "name": "firebase/php-jwt",
2099
-            "version": "v6.10.1",
2099
+            "version": "v6.10.2",
2100 2100
             "source": {
2101 2101
                 "type": "git",
2102 2102
                 "url": "https://github.com/firebase/php-jwt.git",
2103
-                "reference": "500501c2ce893c824c801da135d02661199f60c5"
2103
+                "reference": "30c19ed0f3264cb660ea496895cfb6ef7ee3653b"
2104 2104
             },
2105 2105
             "dist": {
2106 2106
                 "type": "zip",
2107
-                "url": "https://api.github.com/repos/firebase/php-jwt/zipball/500501c2ce893c824c801da135d02661199f60c5",
2108
-                "reference": "500501c2ce893c824c801da135d02661199f60c5",
2107
+                "url": "https://api.github.com/repos/firebase/php-jwt/zipball/30c19ed0f3264cb660ea496895cfb6ef7ee3653b",
2108
+                "reference": "30c19ed0f3264cb660ea496895cfb6ef7ee3653b",
2109 2109
                 "shasum": ""
2110 2110
             },
2111 2111
             "require": {
@@ -2153,9 +2153,9 @@
2153 2153
             ],
2154 2154
             "support": {
2155 2155
                 "issues": "https://github.com/firebase/php-jwt/issues",
2156
-                "source": "https://github.com/firebase/php-jwt/tree/v6.10.1"
2156
+                "source": "https://github.com/firebase/php-jwt/tree/v6.10.2"
2157 2157
             },
2158
-            "time": "2024-05-18T18:05:11+00:00"
2158
+            "time": "2024-11-24T11:22:49+00:00"
2159 2159
         },
2160 2160
         {
2161 2161
             "name": "fruitcake/php-cors",

+ 3
- 3
package-lock.json Ver fichero

@@ -1026,9 +1026,9 @@
1026 1026
             }
1027 1027
         },
1028 1028
         "node_modules/caniuse-lite": {
1029
-            "version": "1.0.30001683",
1030
-            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001683.tgz",
1031
-            "integrity": "sha512-iqmNnThZ0n70mNwvxpEC2nBJ037ZHZUoBI5Gorh1Mw6IlEAZujEoU1tXA628iZfzm7R9FvFzxbfdgml82a3k8Q==",
1029
+            "version": "1.0.30001684",
1030
+            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001684.tgz",
1031
+            "integrity": "sha512-G1LRwLIQjBQoyq0ZJGqGIJUXzJ8irpbjHLpVRXDvBEScFJ9b17sgK6vlx0GAJFE21okD7zXl08rRRUfq6HdoEQ==",
1032 1032
             "dev": true,
1033 1033
             "funding": [
1034 1034
                 {

+ 251
- 0
resources/views/filament/forms/components/line-item-repeater.blade.php Ver fichero

@@ -0,0 +1,251 @@
1
+@php
2
+    use Filament\Forms\Components\Actions\Action;
3
+    use Filament\Support\Enums\Alignment;
4
+    use Filament\Support\Enums\MaxWidth;
5
+
6
+    $containers = $getChildComponentContainersWithoutNestedSchema();
7
+
8
+    $addAction = $getAction($getAddActionName());
9
+    $cloneAction = $getAction($getCloneActionName());
10
+    $deleteAction = $getAction($getDeleteActionName());
11
+    $moveDownAction = $getAction($getMoveDownActionName());
12
+    $moveUpAction = $getAction($getMoveUpActionName());
13
+    $reorderAction = $getAction($getReorderActionName());
14
+    $isReorderableWithButtons = $isReorderableWithButtons();
15
+    $extraItemActions = $getExtraItemActions();
16
+    $extraActions = $getExtraActions();
17
+    $visibleExtraItemActions = [];
18
+    $visibleExtraActions = [];
19
+
20
+    $headers = $getHeaders();
21
+    $renderHeader = $shouldRenderHeader();
22
+    $stackAt = $getStackAt();
23
+    $hasContainers = count($containers) > 0;
24
+    $emptyLabel = $getEmptyLabel();
25
+    $streamlined = $isStreamlined();
26
+
27
+    $statePath = $getStatePath();
28
+
29
+    foreach ($extraActions as $extraAction) {
30
+        $visibleExtraActions = array_filter(
31
+            $extraActions,
32
+            fn (Action $action): bool => $action->isVisible(),
33
+        );
34
+    }
35
+
36
+    foreach ($extraItemActions as $extraItemAction) {
37
+        $visibleExtraItemActions = array_filter(
38
+            $extraItemActions,
39
+            fn (Action $action): bool => $action->isVisible(),
40
+        );
41
+    }
42
+
43
+    $hasActions = $reorderAction->isVisible()
44
+        || $cloneAction->isVisible()
45
+        || $deleteAction->isVisible()
46
+        || $moveUpAction->isVisible()
47
+        || $moveDownAction->isVisible()
48
+        || filled($visibleExtraItemActions);
49
+
50
+    $hasNestedSchema = $hasNestedSchema();
51
+
52
+    $totalColumns = count($headers) + ($hasActions ? 1 : 0);
53
+    $nestedColspan = 4; // Nested schema spans the last 3 columns.
54
+    $emptyColspan = $totalColumns - $nestedColspan;
55
+@endphp
56
+
57
+<x-dynamic-component :component="$getFieldWrapperView()" :field="$field">
58
+    <div
59
+        x-data="{}"
60
+        {{ $attributes->merge($getExtraAttributes())->class([
61
+            'table-repeater-component space-y-6 relative',
62
+            'streamlined' => $streamlined,
63
+            match ($stackAt) {
64
+                'sm', MaxWidth::Small => 'break-point-sm',
65
+                'lg', MaxWidth::Large => 'break-point-lg',
66
+                'xl', MaxWidth::ExtraLarge => 'break-point-xl',
67
+                '2xl', MaxWidth::TwoExtraLarge => 'break-point-2xl',
68
+                default => 'break-point-md',
69
+            }
70
+        ]) }}
71
+    >
72
+        @if (count($containers) || $emptyLabel !== false)
73
+            <div class="table-repeater-container rounded-xl relative ring-1 ring-gray-950/5 dark:ring-white/20">
74
+                <table class="w-full">
75
+                    <thead @class([
76
+                        'table-repeater-header-hidden sr-only' => ! $renderHeader,
77
+                        'table-repeater-header rounded-t-xl overflow-hidden border-b border-gray-950/5 dark:border-white/20' => $renderHeader,
78
+                    ])>
79
+                    <tr class="text-xs md:divide-x md:divide-gray-950/5 dark:md:divide-white/20">
80
+                        @foreach ($headers as $key => $header)
81
+                            <th
82
+                                @class([
83
+                                    'table-repeater-header-column p-2 font-medium first:rounded-tl-xl last:rounded-tr-xl bg-gray-100 dark:text-gray-300 dark:bg-gray-900/60',
84
+                                    match($header->getAlignment()) {
85
+                                      'center', Alignment::Center => 'text-center',
86
+                                      'right', 'end', Alignment::Right, Alignment::End => 'text-end',
87
+                                      default => 'text-start'
88
+                                    }
89
+                                ])
90
+                                style="width: {{ $header->getWidth() }}"
91
+                            >
92
+                                {{ $header->getLabel() }}
93
+                                @if ($header->isRequired())
94
+                                    <span class="whitespace-nowrap">
95
+                                        <sup class="font-medium text-danger-700 dark:text-danger-400">*</sup>
96
+                                    </span>
97
+                                @endif
98
+                            </th>
99
+                        @endforeach
100
+                        @if ($hasActions && count($containers))
101
+                            <th class="table-repeater-header-column w-px last:rounded-tr-xl p-2 bg-gray-100 dark:bg-gray-900/60">
102
+                                <span class="sr-only">
103
+                                    {{ trans('table-repeater::components.repeater.row_actions.label') }}
104
+                                </span>
105
+                            </th>
106
+                        @endif
107
+                    </tr>
108
+                    </thead>
109
+                    <tbody
110
+                        x-sortable
111
+                        wire:end.stop="{{ 'mountFormComponentAction(\'' . $statePath . '\', \'reorder\', { items: $event.target.sortable.toArray() })' }}"
112
+                        class="table-repeater-rows-wrapper divide-y divide-gray-950/5 dark:divide-white/20"
113
+                    >
114
+                    @if (count($containers))
115
+                        @foreach ($containers as $uuid => $row)
116
+                            @php
117
+                                $visibleExtraItemActions = array_filter(
118
+                                    $extraItemActions,
119
+                                    fn (Action $action): bool => $action(['item' => $uuid])->isVisible(),
120
+                                );
121
+                            @endphp
122
+                            <tr
123
+                                wire:key="{{ $this->getId() }}.{{ $row->getStatePath() }}.{{ $field::class }}.item"
124
+                                x-sortable-item="{{ $uuid }}"
125
+                                class="table-repeater-row"
126
+                            >
127
+                                @php($counter = 0)
128
+                                @foreach($row->getComponents() as $cell)
129
+                                    @if($cell instanceof \Filament\Forms\Components\Hidden || $cell->isHidden())
130
+                                        {{ $cell }}
131
+                                    @else
132
+                                        <td
133
+                                            @class([
134
+                                                'table-repeater-column',
135
+                                                'p-2' => ! $streamlined,
136
+                                                'has-hidden-label' => $cell->isLabelHidden(),
137
+                                                match($headers[$counter++]->getAlignment()) {
138
+                                                  'center', Alignment::Center => 'text-center',
139
+                                                  'right', 'end', Alignment::Right, Alignment::End => 'text-end',
140
+                                                  default => 'text-start'
141
+                                                }
142
+                                            ])
143
+                                            style="width: {{ $cell->getMaxWidth() ?? 'auto' }}"
144
+                                        >
145
+                                            {{ $cell }}
146
+                                        </td>
147
+                                    @endif
148
+                                @endforeach
149
+
150
+                                @if ($hasActions)
151
+                                    <td class="table-repeater-column p-2 w-px">
152
+                                        <ul class="flex items-center table-repeater-row-actions gap-x-3 px-2">
153
+                                            @foreach ($visibleExtraItemActions as $extraItemAction)
154
+                                                <li>
155
+                                                    {{ $extraItemAction(['item' => $uuid]) }}
156
+                                                </li>
157
+                                            @endforeach
158
+
159
+                                            @if ($reorderAction->isVisible())
160
+                                                <li x-sortable-handle class="shrink-0">
161
+                                                    {{ $reorderAction }}
162
+                                                </li>
163
+                                            @endif
164
+
165
+                                            @if ($isReorderableWithButtons)
166
+                                                @if (! $loop->first)
167
+                                                    <li>
168
+                                                        {{ $moveUpAction(['item' => $uuid]) }}
169
+                                                    </li>
170
+                                                @endif
171
+
172
+                                                @if (! $loop->last)
173
+                                                    <li>
174
+                                                        {{ $moveDownAction(['item' => $uuid]) }}
175
+                                                    </li>
176
+                                                @endif
177
+                                            @endif
178
+
179
+                                            @if ($cloneAction->isVisible())
180
+                                                <li>
181
+                                                    {{ $cloneAction(['item' => $uuid]) }}
182
+                                                </li>
183
+                                            @endif
184
+
185
+                                            @if ($deleteAction->isVisible())
186
+                                                <li>
187
+                                                    {{ $deleteAction(['item' => $uuid]) }}
188
+                                                </li>
189
+                                            @endif
190
+                                        </ul>
191
+                                    </td>
192
+                                @endif
193
+                            </tr>
194
+                            @if ($hasNestedSchema)
195
+                                <tr class="table-repeater-nested-row">
196
+                                    {{-- Empty cells for the columns before the nested schema --}}
197
+                                    @if ($emptyColspan > 0)
198
+                                        <td colspan="{{ $emptyColspan }}"></td>
199
+                                    @endif
200
+
201
+                                    {{-- Nested schema spanning the last 3 columns --}}
202
+                                    <td colspan="{{ $nestedColspan }}" class="p-4 bg-gray-50 dark:bg-gray-900">
203
+                                        <div class="nested-schema-wrapper">
204
+                                            @foreach ($getNestedSchema() as $nestedComponent)
205
+                                                {{ $nestedComponent }}
206
+                                            @endforeach
207
+                                        </div>
208
+                                    </td>
209
+                                </tr>
210
+                            @endif
211
+                        @endforeach
212
+                    @else
213
+                        <tr class="table-repeater-row table-repeater-empty-row">
214
+                            <td colspan="{{ count($headers) + intval($hasActions) }}"
215
+                                class="table-repeater-column table-repeater-empty-column p-4 w-px text-center italic">
216
+                                {{ $emptyLabel ?: trans('table-repeater::components.repeater.empty.label') }}
217
+                            </td>
218
+                        </tr>
219
+                    @endif
220
+                    </tbody>
221
+                </table>
222
+            </div>
223
+        @endif
224
+
225
+        @if ($addAction->isVisible() || filled($visibleExtraActions))
226
+            <ul
227
+                @class([
228
+                    'relative flex gap-4',
229
+                    match ($getAddActionAlignment()) {
230
+                        Alignment::Start, Alignment::Left => 'justify-start',
231
+                        Alignment::End, Alignment::Right => 'justify-end',
232
+                        default =>  'justify-center',
233
+                    },
234
+                ])
235
+            >
236
+                @if ($addAction->isVisible())
237
+                    <li>
238
+                        {{ $addAction }}
239
+                    </li>
240
+                @endif
241
+                @if (filled($visibleExtraActions))
242
+                    @foreach ($visibleExtraActions as $extraAction)
243
+                        <li>
244
+                            {{ ($extraAction) }}
245
+                        </li>
246
+                    @endforeach
247
+                @endif
248
+            </ul>
249
+        @endif
250
+    </div>
251
+</x-dynamic-component>

+ 1
- 2
vite.config.js Ver fichero

@@ -1,11 +1,10 @@
1
-import { defineConfig } from 'vite';
1
+import {defineConfig} from 'vite';
2 2
 import laravel, {refreshPaths} from 'laravel-vite-plugin';
3 3
 
4 4
 export default defineConfig({
5 5
     plugins: [
6 6
         laravel({
7 7
             input: [
8
-                'resources/css/app.css',
9 8
                 'resources/js/app.js',
10 9
                 'resources/css/filament/company/theme.css',
11 10
                 'resources/css/filament/user/theme.css',

Loading…
Cancelar
Guardar