Andrew Wallo hace 6 meses
padre
commit
8bf3af43a8

+ 3
- 4
app/Filament/Company/Clusters/Settings/Resources/AdjustmentResource.php Ver fichero

@@ -14,7 +14,6 @@ use Filament\Forms\Form;
14 14
 use Filament\Resources\Resource;
15 15
 use Filament\Tables;
16 16
 use Filament\Tables\Table;
17
-use Wallo\FilamentSelectify\Components\ToggleButton;
18 17
 
19 18
 class AdjustmentResource extends Resource
20 19
 {
@@ -33,8 +32,7 @@ class AdjustmentResource extends Resource
33 32
                             ->required()
34 33
                             ->maxLength(255),
35 34
                         Forms\Components\Textarea::make('description')
36
-                            ->label('Description')
37
-                            ->autosize(),
35
+                            ->label('Description'),
38 36
                     ]),
39 37
                 Forms\Components\Section::make('Configuration')
40 38
                     ->schema([
@@ -50,9 +48,10 @@ class AdjustmentResource extends Resource
50 48
                             ->default(AdjustmentType::Sales)
51 49
                             ->live()
52 50
                             ->required(),
53
-                        ToggleButton::make('recoverable')
51
+                        Forms\Components\Checkbox::make('recoverable')
54 52
                             ->label('Recoverable')
55 53
                             ->default(false)
54
+                            ->helperText('When enabled, tax is tracked separately as claimable from the government. Non-recoverable taxes are treated as part of the expense.')
56 55
                             ->visible(fn (Forms\Get $get) => AdjustmentCategory::parse($get('category'))->isTax() && AdjustmentType::parse($get('type'))->isPurchase()),
57 56
                     ])
58 57
                     ->columns()

+ 2
- 1
app/Filament/Company/Pages/Accounting/AccountChart.php Ver fichero

@@ -114,6 +114,7 @@ class AccountChart extends Page
114 114
             ->required()
115 115
             ->live()
116 116
             ->disabledOn('edit')
117
+            ->searchable()
117 118
             ->options($this->getChartSubtypeOptions($useActiveTab))
118 119
             ->afterStateUpdated(static function (?string $state, Set $set): void {
119 120
                 if ($state) {
@@ -282,7 +283,7 @@ class AccountChart extends Page
282 283
     {
283 284
         return Checkbox::make('archived')
284 285
             ->label('Archive account')
285
-            ->helperText('Archived accounts will not be available for selection in transactions.')
286
+            ->helperText('Archived accounts will not be available for selection in transactions, offerings, or other new records.')
286 287
             ->hiddenOn('create');
287 288
     }
288 289
 

+ 24
- 32
app/Filament/Company/Resources/Common/OfferingResource.php Ver fichero

@@ -4,9 +4,12 @@ namespace App\Filament\Company\Resources\Common;
4 4
 
5 5
 use App\Enums\Accounting\AccountCategory;
6 6
 use App\Enums\Accounting\AccountType;
7
+use App\Enums\Accounting\AdjustmentCategory;
8
+use App\Enums\Accounting\AdjustmentType;
7 9
 use App\Enums\Common\OfferingType;
8 10
 use App\Filament\Company\Resources\Common\OfferingResource\Pages;
9
-use App\Models\Accounting\Account;
11
+use App\Filament\Forms\Components\CreateAccountSelect;
12
+use App\Filament\Forms\Components\CreateAdjustmentSelect;
10 13
 use App\Models\Common\Offering;
11 14
 use App\Utilities\Currency\CurrencyAccessor;
12 15
 use Filament\Forms;
@@ -65,63 +68,52 @@ class OfferingResource extends Resource
65 68
                 // Sellable Section
66 69
                 Forms\Components\Section::make('Sale Information')
67 70
                     ->schema([
68
-                        Forms\Components\Select::make('income_account_id')
71
+                        CreateAccountSelect::make('income_account_id')
69 72
                             ->label('Income account')
70
-                            ->options(Account::query()
71
-                                ->where('category', AccountCategory::Revenue)
72
-                                ->where('type', AccountType::OperatingRevenue)
73
-                                ->pluck('name', 'id')
74
-                                ->toArray())
75
-                            ->searchable()
76
-                            ->preload()
73
+                            ->category(AccountCategory::Revenue)
74
+                            ->type(AccountType::OperatingRevenue)
77 75
                             ->required()
78 76
                             ->validationMessages([
79 77
                                 'required' => 'The income account is required for sellable offerings.',
80 78
                             ]),
81
-                        Forms\Components\Select::make('salesTaxes')
79
+                        CreateAdjustmentSelect::make('salesTaxes')
82 80
                             ->label('Sales tax')
83
-                            ->relationship('salesTaxes', 'name')
84
-                            ->preload()
81
+                            ->category(AdjustmentCategory::Tax)
82
+                            ->type(AdjustmentType::Sales)
85 83
                             ->multiple(),
86
-                        Forms\Components\Select::make('salesDiscounts')
84
+                        CreateAdjustmentSelect::make('salesDiscounts')
87 85
                             ->label('Sales discount')
88
-                            ->relationship('salesDiscounts', 'name')
89
-                            ->preload()
86
+                            ->category(AdjustmentCategory::Discount)
87
+                            ->type(AdjustmentType::Sales)
90 88
                             ->multiple(),
91 89
                     ])
92 90
                     ->columns()
93
-                    ->visible(fn (Forms\Get $get) => in_array('Sellable', $get('attributes') ?? [])),
91
+                    ->visible(static fn (Forms\Get $get) => in_array('Sellable', $get('attributes') ?? [])),
94 92
 
95 93
                 // Purchasable Section
96 94
                 Forms\Components\Section::make('Purchase Information')
97 95
                     ->schema([
98
-                        Forms\Components\Select::make('expense_account_id')
96
+                        CreateAccountSelect::make('expense_account_id')
99 97
                             ->label('Expense account')
100
-                            ->options(Account::query()
101
-                                ->where('category', AccountCategory::Expense)
102
-                                ->where('type', AccountType::OperatingExpense)
103
-                                ->orderBy('name')
104
-                                ->pluck('name', 'id')
105
-                                ->toArray())
106
-                            ->searchable()
107
-                            ->preload()
98
+                            ->category(AccountCategory::Expense)
99
+                            ->type(AccountType::OperatingExpense)
108 100
                             ->required()
109 101
                             ->validationMessages([
110 102
                                 'required' => 'The expense account is required for purchasable offerings.',
111 103
                             ]),
112
-                        Forms\Components\Select::make('purchaseTaxes')
104
+                        CreateAdjustmentSelect::make('purchaseTaxes')
113 105
                             ->label('Purchase tax')
114
-                            ->relationship('purchaseTaxes', 'name')
115
-                            ->preload()
106
+                            ->category(AdjustmentCategory::Tax)
107
+                            ->type(AdjustmentType::Purchase)
116 108
                             ->multiple(),
117
-                        Forms\Components\Select::make('purchaseDiscounts')
109
+                        CreateAdjustmentSelect::make('purchaseDiscounts')
118 110
                             ->label('Purchase discount')
119
-                            ->relationship('purchaseDiscounts', 'name')
120
-                            ->preload()
111
+                            ->category(AdjustmentCategory::Discount)
112
+                            ->type(AdjustmentType::Purchase)
121 113
                             ->multiple(),
122 114
                     ])
123 115
                     ->columns()
124
-                    ->visible(fn (Forms\Get $get) => in_array('Purchasable', $get('attributes') ?? [])),
116
+                    ->visible(static fn (Forms\Get $get) => in_array('Purchasable', $get('attributes') ?? [])),
125 117
             ])->columns();
126 118
     }
127 119
 

+ 152
- 0
app/Filament/Forms/Components/CreateAccountSelect.php Ver fichero

@@ -0,0 +1,152 @@
1
+<?php
2
+
3
+namespace App\Filament\Forms\Components;
4
+
5
+use App\Enums\Accounting\AccountCategory;
6
+use App\Enums\Accounting\AccountType;
7
+use App\Models\Accounting\Account;
8
+use App\Models\Accounting\AccountSubtype;
9
+use App\Utilities\Accounting\AccountCode;
10
+use Filament\Forms\Components\Actions\Action;
11
+use Filament\Forms\Components\Select;
12
+use Filament\Forms\Components\Textarea;
13
+use Filament\Forms\Components\TextInput;
14
+use Filament\Forms\Get;
15
+use Filament\Forms\Set;
16
+use Filament\Support\Enums\MaxWidth;
17
+use Illuminate\Support\Collection;
18
+use Illuminate\Support\Facades\DB;
19
+
20
+class CreateAccountSelect extends Select
21
+{
22
+    protected ?AccountCategory $category = null;
23
+
24
+    protected ?AccountType $type = null;
25
+
26
+    protected bool $includeArchived = false;
27
+
28
+    public function category(AccountCategory $category): static
29
+    {
30
+        $this->category = $category;
31
+
32
+        return $this;
33
+    }
34
+
35
+    public function type(AccountType $type): static
36
+    {
37
+        $this->type = $type;
38
+
39
+        return $this;
40
+    }
41
+
42
+    public function includeArchived(bool $includeArchived = true): static
43
+    {
44
+        $this->includeArchived = $includeArchived;
45
+
46
+        return $this;
47
+    }
48
+
49
+    protected function setUp(): void
50
+    {
51
+        parent::setUp();
52
+
53
+        $this
54
+            ->searchable()
55
+            ->live()
56
+            ->createOptionForm($this->createAccountForm())
57
+            ->createOptionAction(fn (Action $action) => $this->createAccountAction($action));
58
+
59
+        $this->options(function () {
60
+            $query = Account::query();
61
+
62
+            if ($this->category) {
63
+                $query->where('category', $this->category);
64
+            }
65
+
66
+            if ($this->type) {
67
+                $query->where('type', $this->type);
68
+            }
69
+
70
+            if (! $this->includeArchived) {
71
+                $query->where('archived', false);
72
+            }
73
+
74
+            return $query->orderBy('name')
75
+                ->pluck('name', 'id')
76
+                ->toArray();
77
+        });
78
+
79
+        $this->createOptionUsing(static function (array $data) {
80
+            return DB::transaction(static function () use ($data) {
81
+                $account = Account::create([
82
+                    'name' => $data['name'],
83
+                    'code' => $data['code'],
84
+                    'description' => $data['description'] ?? null,
85
+                    'subtype_id' => $data['subtype_id'],
86
+                ]);
87
+
88
+                return $account->getKey();
89
+            });
90
+        });
91
+    }
92
+
93
+    protected function createAccountForm(): array
94
+    {
95
+        return [
96
+            Select::make('subtype_id')
97
+                ->label('Type')
98
+                ->required()
99
+                ->live()
100
+                ->searchable()
101
+                ->options(function () {
102
+                    $query = AccountSubtype::query()->orderBy('name');
103
+
104
+                    if ($this->category) {
105
+                        $query->where('category', $this->category);
106
+                    }
107
+
108
+                    if ($this->type) {
109
+                        $query->where('type', $this->type);
110
+
111
+                        return $query->pluck('name', 'id')
112
+                            ->toArray();
113
+                    } else {
114
+                        return $query->get()
115
+                            ->groupBy(fn (AccountSubtype $subtype) => $subtype->type->getLabel())
116
+                            ->map(fn (Collection $subtypes, string $type) => $subtypes->mapWithKeys(static fn (AccountSubtype $subtype) => [$subtype->id => $subtype->name]))
117
+                            ->toArray();
118
+                    }
119
+                })
120
+                ->afterStateUpdated(function (string $state, Set $set) {
121
+                    if ($state) {
122
+                        $accountSubtype = AccountSubtype::find($state);
123
+                        $generatedCode = AccountCode::generate($accountSubtype);
124
+                        $set('code', $generatedCode);
125
+                    }
126
+                }),
127
+
128
+            TextInput::make('code')
129
+                ->label('Code')
130
+                ->required()
131
+                ->validationAttribute('account code')
132
+                ->unique(table: Account::class, column: 'code')
133
+                ->validateAccountCode(static fn (Get $get) => $get('subtype_id')),
134
+
135
+            TextInput::make('name')
136
+                ->label('Name')
137
+                ->required(),
138
+
139
+            Textarea::make('description')
140
+                ->label('Description'),
141
+        ];
142
+    }
143
+
144
+    protected function createAccountAction(Action $action): Action
145
+    {
146
+        return $action
147
+            ->label('Create Account')
148
+            ->slideOver()
149
+            ->modalWidth(MaxWidth::Large)
150
+            ->modalHeading('Create a new account');
151
+    }
152
+}

+ 180
- 0
app/Filament/Forms/Components/CreateAdjustmentSelect.php Ver fichero

@@ -0,0 +1,180 @@
1
+<?php
2
+
3
+namespace App\Filament\Forms\Components;
4
+
5
+use App\Enums\Accounting\AdjustmentCategory;
6
+use App\Enums\Accounting\AdjustmentComputation;
7
+use App\Enums\Accounting\AdjustmentScope;
8
+use App\Enums\Accounting\AdjustmentType;
9
+use App\Models\Accounting\Adjustment;
10
+use Filament\Forms\Components\Actions\Action;
11
+use Filament\Forms\Components\Checkbox;
12
+use Filament\Forms\Components\DateTimePicker;
13
+use Filament\Forms\Components\Group;
14
+use Filament\Forms\Components\Select;
15
+use Filament\Forms\Components\Textarea;
16
+use Filament\Forms\Components\TextInput;
17
+use Filament\Forms\Get;
18
+use Filament\Support\Enums\MaxWidth;
19
+use Illuminate\Database\Eloquent\Builder;
20
+use Illuminate\Support\Facades\DB;
21
+
22
+class CreateAdjustmentSelect extends Select
23
+{
24
+    protected ?AdjustmentCategory $category = null;
25
+
26
+    protected ?AdjustmentType $type = null;
27
+
28
+    public function category(AdjustmentCategory $category): static
29
+    {
30
+        $this->category = $category;
31
+
32
+        return $this;
33
+    }
34
+
35
+    public function type(AdjustmentType $type): static
36
+    {
37
+        $this->type = $type;
38
+
39
+        return $this;
40
+    }
41
+
42
+    public function getCategory(): ?AdjustmentCategory
43
+    {
44
+        return $this->category;
45
+    }
46
+
47
+    public function getType(): ?AdjustmentType
48
+    {
49
+        return $this->type;
50
+    }
51
+
52
+    protected function setUp(): void
53
+    {
54
+        parent::setUp();
55
+
56
+        $this
57
+            ->searchable()
58
+            ->preload()
59
+            ->createOptionForm($this->createAdjustmentForm())
60
+            ->createOptionAction(fn (Action $action) => $this->createAdjustmentAction($action));
61
+
62
+        $this->relationship(
63
+            name: 'adjustments',
64
+            titleAttribute: 'name',
65
+            modifyQueryUsing: function (Builder $query) {
66
+                if ($this->category) {
67
+                    $query->where('category', $this->category);
68
+                }
69
+
70
+                if ($this->type) {
71
+                    $query->where('type', $this->type);
72
+                }
73
+
74
+                return $query->orderBy('name');
75
+            },
76
+        );
77
+
78
+        $this->createOptionUsing(static function (array $data, CreateAdjustmentSelect $component) {
79
+            return DB::transaction(static function () use ($data, $component) {
80
+                $category = $data['category'] ?? $component->getCategory();
81
+                $type = $data['type'] ?? $component->getType();
82
+
83
+                $adjustment = Adjustment::create([
84
+                    'name' => $data['name'],
85
+                    'description' => $data['description'] ?? null,
86
+                    'category' => $category,
87
+                    'type' => $type,
88
+                    'computation' => $data['computation'],
89
+                    'rate' => $data['rate'],
90
+                    'scope' => $data['scope'] ?? null,
91
+                    'recoverable' => $data['recoverable'] ?? false,
92
+                    'start_date' => $data['start_date'] ?? null,
93
+                    'end_date' => $data['end_date'] ?? null,
94
+                ]);
95
+
96
+                return $adjustment->getKey();
97
+            });
98
+        });
99
+    }
100
+
101
+    protected function createAdjustmentForm(): array
102
+    {
103
+        return [
104
+            TextInput::make('name')
105
+                ->label('Name')
106
+                ->required()
107
+                ->maxLength(255),
108
+
109
+            Textarea::make('description')
110
+                ->label('Description'),
111
+
112
+            Select::make('category')
113
+                ->label('Category')
114
+                ->options(AdjustmentCategory::class)
115
+                ->default(AdjustmentCategory::Tax)
116
+                ->hidden(fn () => (bool) $this->getCategory())
117
+                ->live()
118
+                ->required(),
119
+
120
+            Select::make('type')
121
+                ->label('Type')
122
+                ->options(AdjustmentType::class)
123
+                ->default(AdjustmentType::Sales)
124
+                ->hidden(fn () => (bool) $this->getType())
125
+                ->live()
126
+                ->required(),
127
+
128
+            Select::make('computation')
129
+                ->label('Computation')
130
+                ->options(AdjustmentComputation::class)
131
+                ->default(AdjustmentComputation::Percentage)
132
+                ->live()
133
+                ->required(),
134
+
135
+            TextInput::make('rate')
136
+                ->label('Rate')
137
+                ->rate(static fn (Get $get) => $get('computation'))
138
+                ->required(),
139
+
140
+            Select::make('scope')
141
+                ->label('Scope')
142
+                ->options(AdjustmentScope::class),
143
+
144
+            Checkbox::make('recoverable')
145
+                ->label('Recoverable')
146
+                ->default(false)
147
+                ->helperText('When enabled, tax is tracked separately as claimable from the government. Non-recoverable taxes are treated as part of the expense.')
148
+                ->visible(function (Get $get) {
149
+                    $category = $this->getCategory() ?? AdjustmentCategory::parse($get('category'));
150
+                    $type = $this->getType() ?? AdjustmentType::parse($get('type'));
151
+
152
+                    return $category->isTax() && $type->isPurchase();
153
+                }),
154
+
155
+            Group::make()
156
+                ->schema([
157
+                    DateTimePicker::make('start_date'),
158
+                    DateTimePicker::make('end_date'),
159
+                ])
160
+                ->visible(function (Get $get) {
161
+                    $category = $this->getCategory() ?? AdjustmentCategory::parse($get('category'));
162
+
163
+                    return $category->isDiscount();
164
+                }),
165
+        ];
166
+    }
167
+
168
+    protected function createAdjustmentAction(Action $action): Action
169
+    {
170
+        $categoryLabel = $this->getCategory()?->getLabel() ?? 'Adjustment';
171
+        $typeLabel = $this->getType()?->getLabel() ?? '';
172
+        $label = trim($typeLabel . ' ' . $categoryLabel);
173
+
174
+        return $action
175
+            ->label('Create ' . $label)
176
+            ->slideOver()
177
+            ->modalWidth(MaxWidth::ExtraLarge)
178
+            ->modalHeading('Create a new ' . strtolower($label));
179
+    }
180
+}

+ 1
- 1
app/Observers/AdjustmentObserver.php Ver fichero

@@ -9,7 +9,7 @@ class AdjustmentObserver
9 9
 {
10 10
     public function creating(Adjustment $adjustment): void
11 11
     {
12
-        if ($adjustment->account_id === null && ! $adjustment->isNonRecoverablePurchaseTax()) {
12
+        if (! $adjustment->account_id && ! $adjustment->isNonRecoverablePurchaseTax()) {
13 13
             $account = null;
14 14
 
15 15
             if ($adjustment->isSalesTax()) {

+ 0
- 4
resources/css/filament/company/custom-section.css Ver fichero

@@ -48,10 +48,6 @@
48 48
     &.fi-section-not-contained:not(.fi-aside) {
49 49
         @apply grid gap-y-4;
50 50
 
51
-        & .fi-section-header {
52
-            @apply py-2;
53
-        }
54
-
55 51
         & .fi-section-content-ctn {
56 52
             @apply grid gap-y-4;
57 53
         }

Loading…
Cancelar
Guardar