瀏覽代碼

wip budgets

3.x
Andrew Wallo 7 月之前
父節點
當前提交
fe0760032d

+ 0
- 7
app/Enums/Accounting/BudgetIntervalType.php 查看文件

@@ -9,7 +9,6 @@ enum BudgetIntervalType: string implements HasLabel
9 9
 {
10 10
     use ParsesEnum;
11 11
 
12
-    case Week = 'week';
13 12
     case Month = 'month';
14 13
     case Quarter = 'quarter';
15 14
     case Year = 'year';
@@ -17,18 +16,12 @@ enum BudgetIntervalType: string implements HasLabel
17 16
     public function getLabel(): ?string
18 17
     {
19 18
         return match ($this) {
20
-            self::Week => 'Weekly',
21 19
             self::Month => 'Monthly',
22 20
             self::Quarter => 'Quarterly',
23 21
             self::Year => 'Yearly',
24 22
         };
25 23
     }
26 24
 
27
-    public function isWeek(): bool
28
-    {
29
-        return $this === self::Week;
30
-    }
31
-
32 25
     public function isMonth(): bool
33 26
     {
34 27
         return $this === self::Month;

+ 145
- 160
app/Filament/Company/Resources/Accounting/BudgetResource.php 查看文件

@@ -46,7 +46,6 @@ class BudgetResource extends Resource
46 46
                             ->live()
47 47
                             ->disabled(static fn (Forms\Get $get) => blank($get('start_date')))
48 48
                             ->minDate(fn (Forms\Get $get) => match (BudgetIntervalType::parse($get('interval_type'))) {
49
-                                BudgetIntervalType::Week => Carbon::parse($get('start_date'))->addWeek(),
50 49
                                 BudgetIntervalType::Month => Carbon::parse($get('start_date'))->addMonth(),
51 50
                                 BudgetIntervalType::Quarter => Carbon::parse($get('start_date'))->addQuarter(),
52 51
                                 BudgetIntervalType::Year => Carbon::parse($get('start_date'))->addYear(),
@@ -57,144 +56,144 @@ class BudgetResource extends Resource
57 56
                             ->columnSpanFull(),
58 57
                     ]),
59 58
 
60
-                Forms\Components\Section::make('Budget Items')
61
-                    ->headerActions([
62
-                        Forms\Components\Actions\Action::make('addAccounts')
63
-                            ->label('Add Accounts')
64
-                            ->icon('heroicon-m-plus')
65
-                            ->outlined()
66
-                            ->color('primary')
67
-                            ->form(fn (Forms\Get $get) => [
68
-                                Forms\Components\Select::make('selected_accounts')
69
-                                    ->label('Choose Accounts to Add')
70
-                                    ->options(function () use ($get) {
71
-                                        $existingAccounts = collect($get('budgetItems'))->pluck('account_id')->toArray();
72
-
73
-                                        return Account::query()
74
-                                            ->budgetable()
75
-                                            ->whereNotIn('id', $existingAccounts) // Prevent duplicate selections
76
-                                            ->pluck('name', 'id');
77
-                                    })
78
-                                    ->searchable()
79
-                                    ->multiple()
80
-                                    ->hint('Select the accounts you want to add to this budget'),
81
-                            ])
82
-                            ->action(static fn (Forms\Set $set, Forms\Get $get, array $data) => self::addSelectedAccounts($set, $get, $data)),
83
-
84
-                        Forms\Components\Actions\Action::make('addAllAccounts')
85
-                            ->label('Add All Accounts')
86
-                            ->icon('heroicon-m-folder-plus')
87
-                            ->outlined()
88
-                            ->color('primary')
89
-                            ->action(static fn (Forms\Set $set, Forms\Get $get) => self::addAllAccounts($set, $get))
90
-                            ->hidden(static fn (Forms\Get $get) => filled($get('budgetItems'))),
91
-
92
-                        Forms\Components\Actions\Action::make('increaseAllocations')
93
-                            ->label('Increase Allocations')
94
-                            ->icon('heroicon-m-arrow-up')
95
-                            ->outlined()
96
-                            ->color('success')
97
-                            ->form(fn (Forms\Get $get) => [
98
-                                Forms\Components\Select::make('increase_type')
99
-                                    ->label('Increase Type')
100
-                                    ->options([
101
-                                        'percentage' => 'Percentage (%)',
102
-                                        'fixed' => 'Fixed Amount',
103
-                                    ])
104
-                                    ->default('percentage')
105
-                                    ->live()
106
-                                    ->required(),
107
-
108
-                                Forms\Components\TextInput::make('percentage')
109
-                                    ->label('Increase by %')
110
-                                    ->numeric()
111
-                                    ->suffix('%')
112
-                                    ->required()
113
-                                    ->hidden(fn (Forms\Get $get) => $get('increase_type') !== 'percentage'),
114
-
115
-                                Forms\Components\TextInput::make('fixed_amount')
116
-                                    ->label('Increase by Fixed Amount')
117
-                                    ->numeric()
118
-                                    ->suffix('USD')
119
-                                    ->required()
120
-                                    ->hidden(fn (Forms\Get $get) => $get('increase_type') !== 'fixed'),
121
-
122
-                                Forms\Components\Select::make('apply_to_accounts')
123
-                                    ->label('Apply to Accounts')
124
-                                    ->options(function () use ($get) {
125
-                                        $budgetItems = $get('budgetItems') ?? [];
126
-                                        $accountIds = collect($budgetItems)
127
-                                            ->pluck('account_id')
128
-                                            ->filter()
129
-                                            ->unique()
130
-                                            ->toArray();
131
-
132
-                                        return Account::query()
133
-                                            ->whereIn('id', $accountIds)
134
-                                            ->pluck('name', 'id')
135
-                                            ->toArray();
136
-                                    })
137
-                                    ->searchable()
138
-                                    ->multiple()
139
-                                    ->hint('Leave blank to apply to all accounts'),
140
-
141
-                                Forms\Components\Select::make('apply_to_periods')
142
-                                    ->label('Apply to Periods')
143
-                                    ->options(static function () use ($get) {
144
-                                        $startDate = $get('start_date');
145
-                                        $endDate = $get('end_date');
146
-                                        $intervalType = $get('interval_type');
147
-
148
-                                        if (blank($startDate) || blank($endDate) || blank($intervalType)) {
149
-                                            return [];
150
-                                        }
151
-
152
-                                        $labels = self::generateFormattedLabels($startDate, $endDate, $intervalType);
153
-
154
-                                        return array_combine($labels, $labels);
155
-                                    })
156
-                                    ->searchable()
157
-                                    ->multiple()
158
-                                    ->hint('Leave blank to apply to all periods'),
159
-                            ])
160
-                            ->action(static fn (Forms\Set $set, Forms\Get $get, array $data) => self::increaseAllocations($set, $get, $data))
161
-                            ->visible(static fn (Forms\Get $get) => filled($get('budgetItems'))),
162
-                    ])
163
-                    ->schema([
164
-                        Forms\Components\Repeater::make('budgetItems')
165
-                            ->columns(4)
166
-                            ->hiddenLabel()
167
-                            ->schema([
168
-                                Forms\Components\Select::make('account_id')
169
-                                    ->label('Account')
170
-                                    ->options(Account::query()
171
-                                        ->budgetable()
172
-                                        ->pluck('name', 'id'))
173
-                                    ->searchable()
174
-                                    ->disableOptionsWhenSelectedInSiblingRepeaterItems()
175
-                                    ->columnSpan(1)
176
-                                    ->required(),
177
-
178
-                                Forms\Components\TextInput::make('total_amount')
179
-                                    ->label('Total Amount')
180
-                                    ->numeric()
181
-                                    ->columnSpan(1)
182
-                                    ->suffixAction(
183
-                                        Forms\Components\Actions\Action::make('disperse')
184
-                                            ->label('Disperse')
185
-                                            ->icon('heroicon-m-bars-arrow-down')
186
-                                            ->color('primary')
187
-                                            ->action(static fn (Forms\Set $set, Forms\Get $get, $state) => self::disperseTotalAmount($set, $get, $state))
188
-                                    ),
189
-
190
-                                CustomSection::make('Budget Allocations')
191
-                                    ->contained(false)
192
-                                    ->columns(4)
193
-                                    ->schema(static fn (Forms\Get $get) => self::getAllocationFields($get('../../start_date'), $get('../../end_date'), $get('../../interval_type'))),
194
-                            ])
195
-                            ->defaultItems(0)
196
-                            ->addActionLabel('Add Budget Item'),
197
-                    ]),
59
+                //                Forms\Components\Section::make('Budget Items')
60
+                //                    ->headerActions([
61
+                //                        Forms\Components\Actions\Action::make('addAccounts')
62
+                //                            ->label('Add Accounts')
63
+                //                            ->icon('heroicon-m-plus')
64
+                //                            ->outlined()
65
+                //                            ->color('primary')
66
+                //                            ->form(fn (Forms\Get $get) => [
67
+                //                                Forms\Components\Select::make('selected_accounts')
68
+                //                                    ->label('Choose Accounts to Add')
69
+                //                                    ->options(function () use ($get) {
70
+                //                                        $existingAccounts = collect($get('budgetItems'))->pluck('account_id')->toArray();
71
+                //
72
+                //                                        return Account::query()
73
+                //                                            ->budgetable()
74
+                //                                            ->whereNotIn('id', $existingAccounts) // Prevent duplicate selections
75
+                //                                            ->pluck('name', 'id');
76
+                //                                    })
77
+                //                                    ->searchable()
78
+                //                                    ->multiple()
79
+                //                                    ->hint('Select the accounts you want to add to this budget'),
80
+                //                            ])
81
+                //                            ->action(static fn (Forms\Set $set, Forms\Get $get, array $data) => self::addSelectedAccounts($set, $get, $data)),
82
+                //
83
+                //                        Forms\Components\Actions\Action::make('addAllAccounts')
84
+                //                            ->label('Add All Accounts')
85
+                //                            ->icon('heroicon-m-folder-plus')
86
+                //                            ->outlined()
87
+                //                            ->color('primary')
88
+                //                            ->action(static fn (Forms\Set $set, Forms\Get $get) => self::addAllAccounts($set, $get))
89
+                //                            ->hidden(static fn (Forms\Get $get) => filled($get('budgetItems'))),
90
+                //
91
+                //                        Forms\Components\Actions\Action::make('increaseAllocations')
92
+                //                            ->label('Increase Allocations')
93
+                //                            ->icon('heroicon-m-arrow-up')
94
+                //                            ->outlined()
95
+                //                            ->color('success')
96
+                //                            ->form(fn (Forms\Get $get) => [
97
+                //                                Forms\Components\Select::make('increase_type')
98
+                //                                    ->label('Increase Type')
99
+                //                                    ->options([
100
+                //                                        'percentage' => 'Percentage (%)',
101
+                //                                        'fixed' => 'Fixed Amount',
102
+                //                                    ])
103
+                //                                    ->default('percentage')
104
+                //                                    ->live()
105
+                //                                    ->required(),
106
+                //
107
+                //                                Forms\Components\TextInput::make('percentage')
108
+                //                                    ->label('Increase by %')
109
+                //                                    ->numeric()
110
+                //                                    ->suffix('%')
111
+                //                                    ->required()
112
+                //                                    ->hidden(fn (Forms\Get $get) => $get('increase_type') !== 'percentage'),
113
+                //
114
+                //                                Forms\Components\TextInput::make('fixed_amount')
115
+                //                                    ->label('Increase by Fixed Amount')
116
+                //                                    ->numeric()
117
+                //                                    ->suffix('USD')
118
+                //                                    ->required()
119
+                //                                    ->hidden(fn (Forms\Get $get) => $get('increase_type') !== 'fixed'),
120
+                //
121
+                //                                Forms\Components\Select::make('apply_to_accounts')
122
+                //                                    ->label('Apply to Accounts')
123
+                //                                    ->options(function () use ($get) {
124
+                //                                        $budgetItems = $get('budgetItems') ?? [];
125
+                //                                        $accountIds = collect($budgetItems)
126
+                //                                            ->pluck('account_id')
127
+                //                                            ->filter()
128
+                //                                            ->unique()
129
+                //                                            ->toArray();
130
+                //
131
+                //                                        return Account::query()
132
+                //                                            ->whereIn('id', $accountIds)
133
+                //                                            ->pluck('name', 'id')
134
+                //                                            ->toArray();
135
+                //                                    })
136
+                //                                    ->searchable()
137
+                //                                    ->multiple()
138
+                //                                    ->hint('Leave blank to apply to all accounts'),
139
+                //
140
+                //                                Forms\Components\Select::make('apply_to_periods')
141
+                //                                    ->label('Apply to Periods')
142
+                //                                    ->options(static function () use ($get) {
143
+                //                                        $startDate = $get('start_date');
144
+                //                                        $endDate = $get('end_date');
145
+                //                                        $intervalType = $get('interval_type');
146
+                //
147
+                //                                        if (blank($startDate) || blank($endDate) || blank($intervalType)) {
148
+                //                                            return [];
149
+                //                                        }
150
+                //
151
+                //                                        $labels = self::generateFormattedLabels($startDate, $endDate, $intervalType);
152
+                //
153
+                //                                        return array_combine($labels, $labels);
154
+                //                                    })
155
+                //                                    ->searchable()
156
+                //                                    ->multiple()
157
+                //                                    ->hint('Leave blank to apply to all periods'),
158
+                //                            ])
159
+                //                            ->action(static fn (Forms\Set $set, Forms\Get $get, array $data) => self::increaseAllocations($set, $get, $data))
160
+                //                            ->visible(static fn (Forms\Get $get) => filled($get('budgetItems'))),
161
+                //                    ])
162
+                //                    ->schema([
163
+                //                        Forms\Components\Repeater::make('budgetItems')
164
+                //                            ->columns(4)
165
+                //                            ->hiddenLabel()
166
+                //                            ->schema([
167
+                //                                Forms\Components\Select::make('account_id')
168
+                //                                    ->label('Account')
169
+                //                                    ->options(Account::query()
170
+                //                                        ->budgetable()
171
+                //                                        ->pluck('name', 'id'))
172
+                //                                    ->searchable()
173
+                //                                    ->disableOptionsWhenSelectedInSiblingRepeaterItems()
174
+                //                                    ->columnSpan(1)
175
+                //                                    ->required(),
176
+                //
177
+                //                                Forms\Components\TextInput::make('total_amount')
178
+                //                                    ->label('Total Amount')
179
+                //                                    ->numeric()
180
+                //                                    ->columnSpan(1)
181
+                //                                    ->suffixAction(
182
+                //                                        Forms\Components\Actions\Action::make('disperse')
183
+                //                                            ->label('Disperse')
184
+                //                                            ->icon('heroicon-m-bars-arrow-down')
185
+                //                                            ->color('primary')
186
+                //                                            ->action(static fn (Forms\Set $set, Forms\Get $get, $state) => self::disperseTotalAmount($set, $get, $state))
187
+                //                                    ),
188
+                //
189
+                //                                CustomSection::make('Budget Allocations')
190
+                //                                    ->contained(false)
191
+                //                                    ->columns(4)
192
+                //                                    ->schema(static fn (Forms\Get $get) => self::getAllocationFields($get('../../start_date'), $get('../../end_date'), $get('../../interval_type'))),
193
+                //                            ])
194
+                //                            ->defaultItems(0)
195
+                //                            ->addActionLabel('Add Budget Item'),
196
+                //                    ]),
198 197
             ]);
199 198
     }
200 199
 
@@ -206,6 +205,11 @@ class BudgetResource extends Resource
206 205
                     ->sortable()
207 206
                     ->searchable(),
208 207
 
208
+                Tables\Columns\TextColumn::make('status')
209
+                    ->label('Status')
210
+                    ->sortable()
211
+                    ->badge(),
212
+
209 213
                 Tables\Columns\TextColumn::make('interval_type')
210 214
                     ->label('Interval')
211 215
                     ->sortable()
@@ -373,7 +377,6 @@ class BudgetResource extends Resource
373 377
 
374 378
         while ($start->lte($end)) {
375 379
             $labels[] = match ($intervalTypeEnum) {
376
-                BudgetIntervalType::Week => 'W' . $start->weekOfYear . ' ' . $start->year, // Example: W10 2024
377 380
                 BudgetIntervalType::Month => $start->format('M'), // Example: Jan, Feb, Mar
378 381
                 BudgetIntervalType::Quarter => 'Q' . $start->quarter, // Example: Q1, Q2, Q3
379 382
                 BudgetIntervalType::Year => (string) $start->year, // Example: 2024, 2025
@@ -381,7 +384,6 @@ class BudgetResource extends Resource
381 384
             };
382 385
 
383 386
             match ($intervalTypeEnum) {
384
-                BudgetIntervalType::Week => $start->addWeek(),
385 387
                 BudgetIntervalType::Month => $start->addMonth(),
386 388
                 BudgetIntervalType::Quarter => $start->addQuarter(),
387 389
                 BudgetIntervalType::Year => $start->addYear(),
@@ -398,32 +400,15 @@ class BudgetResource extends Resource
398 400
             return [];
399 401
         }
400 402
 
401
-        $start = Carbon::parse($startDate);
402
-        $end = Carbon::parse($endDate);
403
-        $intervalTypeEnum = BudgetIntervalType::parse($intervalType);
404 403
         $fields = [];
405 404
 
406
-        while ($start->lte($end)) {
407
-            $label = match ($intervalTypeEnum) {
408
-                BudgetIntervalType::Week => 'W' . $start->weekOfYear . ' ' . $start->year, // Example: W10 2024
409
-                BudgetIntervalType::Month => $start->format('M'), // Example: Jan, Feb, Mar
410
-                BudgetIntervalType::Quarter => 'Q' . $start->quarter, // Example: Q1, Q2, Q3
411
-                BudgetIntervalType::Year => (string) $start->year, // Example: 2024, 2025
412
-                default => '',
413
-            };
405
+        $labels = self::generateFormattedLabels($startDate, $endDate, $intervalType);
414 406
 
407
+        foreach ($labels as $label) {
415 408
             $fields[] = Forms\Components\TextInput::make("amounts.{$label}")
416 409
                 ->label($label)
417 410
                 ->numeric()
418 411
                 ->required();
419
-
420
-            match ($intervalTypeEnum) {
421
-                BudgetIntervalType::Week => $start->addWeek(),
422
-                BudgetIntervalType::Month => $start->addMonth(),
423
-                BudgetIntervalType::Quarter => $start->addQuarter(),
424
-                BudgetIntervalType::Year => $start->addYear(),
425
-                default => null,
426
-            };
427 412
         }
428 413
 
429 414
         return $fields;

+ 240
- 4
app/Filament/Company/Resources/Accounting/BudgetResource/Pages/CreateBudget.php 查看文件

@@ -3,17 +3,167 @@
3 3
 namespace App\Filament\Company\Resources\Accounting\BudgetResource\Pages;
4 4
 
5 5
 use App\Enums\Accounting\BudgetIntervalType;
6
+use App\Facades\Accounting;
6 7
 use App\Filament\Company\Resources\Accounting\BudgetResource;
8
+use App\Models\Accounting\Account;
7 9
 use App\Models\Accounting\Budget;
10
+use App\Models\Accounting\BudgetAllocation;
8 11
 use App\Models\Accounting\BudgetItem;
12
+use Filament\Forms;
13
+use Filament\Forms\Components\Actions\Action;
14
+use Filament\Forms\Components\Wizard\Step;
9 15
 use Filament\Resources\Pages\CreateRecord;
16
+use Illuminate\Database\Eloquent\Builder;
10 17
 use Illuminate\Database\Eloquent\Model;
11 18
 use Illuminate\Support\Carbon;
12 19
 
13 20
 class CreateBudget extends CreateRecord
14 21
 {
22
+    use CreateRecord\Concerns\HasWizard;
23
+
15 24
     protected static string $resource = BudgetResource::class;
16 25
 
26
+    public function getSteps(): array
27
+    {
28
+        return [
29
+            Step::make('General Information')
30
+                ->columns(2)
31
+                ->schema([
32
+                    Forms\Components\TextInput::make('name')
33
+                        ->required()
34
+                        ->maxLength(255),
35
+                    Forms\Components\Select::make('interval_type')
36
+                        ->label('Budget Interval')
37
+                        ->options(BudgetIntervalType::class)
38
+                        ->default(BudgetIntervalType::Month->value)
39
+                        ->required()
40
+                        ->live(),
41
+                    Forms\Components\DatePicker::make('start_date')
42
+                        ->required()
43
+                        ->default(now()->startOfYear())
44
+                        ->live(),
45
+                    Forms\Components\DatePicker::make('end_date')
46
+                        ->required()
47
+                        ->default(now()->endOfYear())
48
+                        ->live()
49
+                        ->disabled(static fn (Forms\Get $get) => blank($get('start_date')))
50
+                        ->minDate(fn (Forms\Get $get) => match (BudgetIntervalType::parse($get('interval_type'))) {
51
+                            BudgetIntervalType::Month => Carbon::parse($get('start_date'))->addMonth(),
52
+                            BudgetIntervalType::Quarter => Carbon::parse($get('start_date'))->addQuarter(),
53
+                            BudgetIntervalType::Year => Carbon::parse($get('start_date'))->addYear(),
54
+                            default => Carbon::parse($get('start_date'))->addDay(),
55
+                        })
56
+                        ->maxDate(fn (Forms\Get $get) => Carbon::parse($get('start_date'))->endOfYear()),
57
+                ]),
58
+
59
+            Step::make('Budget Setup & Settings')
60
+                ->schema([
61
+                    // Prefill configuration
62
+                    Forms\Components\Toggle::make('prefill_data')
63
+                        ->label('Prefill Data')
64
+                        ->helperText('Enable this option to prefill the budget with historical data')
65
+                        ->default(false)
66
+                        ->live(),
67
+
68
+                    Forms\Components\Grid::make(1)
69
+                        ->schema([
70
+                            Forms\Components\Select::make('prefill_method')
71
+                                ->label('Prefill Method')
72
+                                ->options([
73
+                                    'previous_budget' => 'Copy from a previous budget',
74
+                                    'actuals' => 'Use historical actuals',
75
+                                ])
76
+                                ->live()
77
+                                ->required(),
78
+
79
+                            // If user selects to copy a previous budget
80
+                            Forms\Components\Select::make('source_budget_id')
81
+                                ->label('Source Budget')
82
+                                ->options(fn () => Budget::query()
83
+                                    ->orderByDesc('end_date')
84
+                                    ->pluck('name', 'id'))
85
+                                ->searchable()
86
+                                ->required()
87
+                                ->visible(fn (Forms\Get $get) => $get('prefill_method') === 'previous_budget'),
88
+
89
+                            // If user selects to use historical actuals
90
+                            Forms\Components\Select::make('actuals_fiscal_year')
91
+                                ->label('Reference Fiscal Year')
92
+                                ->options(function () {
93
+                                    $options = [];
94
+                                    $company = auth()->user()->currentCompany;
95
+                                    $earliestDate = Carbon::parse(Accounting::getEarliestTransactionDate());
96
+                                    $fiscalYearStartCurrent = Carbon::parse($company->locale->fiscalYearStartDate());
97
+
98
+                                    for ($year = $fiscalYearStartCurrent->year; $year >= $earliestDate->year; $year--) {
99
+                                        $options[$year] = $year;
100
+                                    }
101
+
102
+                                    return $options;
103
+                                })
104
+                                ->required()
105
+                                ->live()
106
+                                ->visible(fn (Forms\Get $get) => $get('prefill_method') === 'actuals'),
107
+                        ])->visible(fn (Forms\Get $get) => $get('prefill_data') === true),
108
+
109
+                    Forms\Components\Textarea::make('notes')
110
+                        ->label('Notes')
111
+                        ->columnSpanFull(),
112
+                ]),
113
+
114
+            Step::make('Modify Budget Structure')
115
+                ->schema([
116
+                    Forms\Components\CheckboxList::make('selected_accounts')
117
+                        ->label('Select Accounts to Exclude')
118
+                        ->options(function (Forms\Get $get) {
119
+                            $fiscalYear = $get('actuals_fiscal_year');
120
+
121
+                            // Get all budgetable accounts
122
+                            $allAccounts = Account::query()->budgetable()->pluck('name', 'id')->toArray();
123
+
124
+                            // Get accounts that have actuals for the selected fiscal year
125
+                            $accountsWithActuals = Account::query()
126
+                                ->budgetable()
127
+                                ->whereHas('journalEntries.transaction', function (Builder $query) use ($fiscalYear) {
128
+                                    $query->whereYear('posted_at', $fiscalYear);
129
+                                })
130
+                                ->pluck('name', 'id')
131
+                                ->toArray();
132
+
133
+                            return $allAccounts + $accountsWithActuals; // Merge both sets
134
+                        })
135
+                        ->columns(2) // Display in two columns
136
+                        ->searchable() // Allow searching for accounts
137
+                        ->bulkToggleable() // Enable "Select All" / "Deselect All"
138
+                        ->selectAllAction(
139
+                            fn (Action $action, Forms\Get $get) => $action
140
+                                ->label('Remove all items without past actuals (' .
141
+                                    Account::query()->budgetable()->whereDoesntHave('journalEntries.transaction', function (Builder $query) use ($get) {
142
+                                        $query->whereYear('posted_at', $get('actuals_fiscal_year'));
143
+                                    })->count() . ' lines)')
144
+                        )
145
+                        ->disableOptionWhen(fn (string $value, Forms\Get $get) => in_array(
146
+                            $value,
147
+                            Account::query()->budgetable()->whereHas('journalEntries.transaction', function (Builder $query) use ($get) {
148
+                                $query->whereYear('posted_at', Carbon::parse($get('actuals_fiscal_year'))->year);
149
+                            })->pluck('id')->toArray()
150
+                        ))
151
+                        ->visible(fn (Forms\Get $get) => $get('prefill_method') === 'actuals'),
152
+                ])
153
+                ->visible(function (Forms\Get $get) {
154
+                    $prefillMethod = $get('prefill_method');
155
+
156
+                    if ($prefillMethod !== 'actuals' || blank($get('actuals_fiscal_year'))) {
157
+                        return false;
158
+                    }
159
+
160
+                    return Account::query()->budgetable()->whereDoesntHave('journalEntries.transaction', function (Builder $query) use ($get) {
161
+                        $query->whereYear('posted_at', $get('actuals_fiscal_year'));
162
+                    })->exists();
163
+                }),
164
+        ];
165
+    }
166
+
17 167
     protected function handleRecordCreation(array $data): Model
18 168
     {
19 169
         /** @var Budget $budget */
@@ -25,15 +175,29 @@ class CreateBudget extends CreateRecord
25 175
             'notes' => $data['notes'] ?? null,
26 176
         ]);
27 177
 
28
-        foreach ($data['budgetItems'] as $itemData) {
178
+        $selectedAccounts = $data['selected_accounts'] ?? [];
179
+
180
+        $accountsToInclude = Account::query()
181
+            ->budgetable()
182
+            ->whereNotIn('id', $selectedAccounts)
183
+            ->get();
184
+
185
+        foreach ($accountsToInclude as $account) {
29 186
             /** @var BudgetItem $budgetItem */
30 187
             $budgetItem = $budget->budgetItems()->create([
31
-                'account_id' => $itemData['account_id'],
188
+                'account_id' => $account->id,
32 189
             ]);
33 190
 
34 191
             $allocationStart = Carbon::parse($data['start_date']);
35 192
 
36
-            foreach ($itemData['amounts'] as $periodLabel => $amount) {
193
+            // Determine amounts based on the prefill method
194
+            $amounts = match ($data['prefill_method'] ?? null) {
195
+                'actuals' => $this->getAmountsFromActuals($account, $data['actuals_fiscal_year'], BudgetIntervalType::parse($data['interval_type'])),
196
+                'previous_budget' => $this->getAmountsFromPreviousBudget($account, $data['source_budget_id'], BudgetIntervalType::parse($data['interval_type'])),
197
+                default => $this->generateZeroAmounts($data['start_date'], $data['end_date'], BudgetIntervalType::parse($data['interval_type'])),
198
+            };
199
+
200
+            foreach ($amounts as $periodLabel => $amount) {
37 201
                 $allocationEnd = self::calculateEndDate($allocationStart, BudgetIntervalType::parse($data['interval_type']));
38 202
 
39 203
                 $budgetItem->allocations()->create([
@@ -51,10 +215,82 @@ class CreateBudget extends CreateRecord
51 215
         return $budget;
52 216
     }
53 217
 
218
+    private function getAmountsFromActuals(Account $account, int $fiscalYear, BudgetIntervalType $intervalType): array
219
+    {
220
+        // Determine the fiscal year start and end dates
221
+        $fiscalYearStart = Carbon::create($fiscalYear, 1, 1)->startOfYear();
222
+        $fiscalYearEnd = $fiscalYearStart->copy()->endOfYear();
223
+
224
+        $netMovement = Accounting::getNetMovement($account, $fiscalYearStart->toDateString(), $fiscalYearEnd->toDateString());
225
+
226
+        return $this->distributeAmountAcrossPeriods($netMovement->getAmount(), $fiscalYearStart, $fiscalYearEnd, $intervalType);
227
+    }
228
+
229
+    private function distributeAmountAcrossPeriods(float $totalAmount, Carbon $startDate, Carbon $endDate, BudgetIntervalType $intervalType): array
230
+    {
231
+        $amounts = [];
232
+        $periods = [];
233
+
234
+        // Generate period labels based on interval type
235
+        $currentPeriod = $startDate->copy();
236
+        while ($currentPeriod->lte($endDate)) {
237
+            $periods[] = $this->determinePeriod($currentPeriod, $intervalType);
238
+            $currentPeriod->addUnit($intervalType->value);
239
+        }
240
+
241
+        // Evenly distribute total amount across periods
242
+        $periodCount = count($periods);
243
+        $amountPerPeriod = $periodCount > 0 ? round($totalAmount / $periodCount, 2) : 0;
244
+
245
+        foreach ($periods as $periodLabel) {
246
+            $amounts[$periodLabel] = $amountPerPeriod;
247
+        }
248
+
249
+        return $amounts;
250
+    }
251
+
252
+    private function getAmountsFromPreviousBudget(Account $account, int $sourceBudgetId, BudgetIntervalType $intervalType): array
253
+    {
254
+        $amounts = [];
255
+
256
+        $previousAllocations = BudgetAllocation::query()
257
+            ->whereHas('budgetItem', fn ($query) => $query->where('account_id', $account->id)->where('budget_id', $sourceBudgetId))
258
+            ->get();
259
+
260
+        foreach ($previousAllocations as $allocation) {
261
+            $amounts[$allocation->period] = $allocation->amount;
262
+        }
263
+
264
+        return $amounts;
265
+    }
266
+
267
+    private function generateZeroAmounts(string $startDate, string $endDate, BudgetIntervalType $intervalType): array
268
+    {
269
+        $amounts = [];
270
+
271
+        $currentPeriod = Carbon::parse($startDate);
272
+        while ($currentPeriod->lte(Carbon::parse($endDate))) {
273
+            $period = $this->determinePeriod($currentPeriod, $intervalType);
274
+            $amounts[$period] = 0.00;
275
+            $currentPeriod->addUnit($intervalType->value);
276
+        }
277
+
278
+        return $amounts;
279
+    }
280
+
281
+    private function determinePeriod(Carbon $date, BudgetIntervalType $intervalType): string
282
+    {
283
+        return match ($intervalType) {
284
+            BudgetIntervalType::Month => $date->format('F Y'),
285
+            BudgetIntervalType::Quarter => 'Q' . $date->quarter . ' ' . $date->year,
286
+            BudgetIntervalType::Year => (string) $date->year,
287
+            default => $date->format('Y-m-d'),
288
+        };
289
+    }
290
+
54 291
     private static function calculateEndDate(Carbon $startDate, BudgetIntervalType $intervalType): Carbon
55 292
     {
56 293
         return match ($intervalType) {
57
-            BudgetIntervalType::Week => $startDate->copy()->endOfWeek(),
58 294
             BudgetIntervalType::Month => $startDate->copy()->endOfMonth(),
59 295
             BudgetIntervalType::Quarter => $startDate->copy()->endOfQuarter(),
60 296
             BudgetIntervalType::Year => $startDate->copy()->endOfYear(),

+ 0
- 1
app/Filament/Company/Resources/Accounting/BudgetResource/Pages/EditBudget.php 查看文件

@@ -95,7 +95,6 @@ class EditBudget extends EditRecord
95 95
     private static function calculateEndDate(Carbon $startDate, BudgetIntervalType $intervalType): Carbon
96 96
     {
97 97
         return match ($intervalType) {
98
-            BudgetIntervalType::Week => $startDate->copy()->endOfWeek(),
99 98
             BudgetIntervalType::Month => $startDate->copy()->endOfMonth(),
100 99
             BudgetIntervalType::Quarter => $startDate->copy()->endOfQuarter(),
101 100
             BudgetIntervalType::Year => $startDate->copy()->endOfYear(),

+ 3
- 4
app/Models/Accounting/Account.php 查看文件

@@ -88,12 +88,11 @@ class Account extends Model
88 88
 
89 89
     public function scopeBudgetable(Builder $query): Builder
90 90
     {
91
-        return $query->whereNotIn('category', [
92
-            AccountCategory::Equity,
93
-            AccountCategory::Liability,
91
+        return $query->whereIn('category', [
92
+            AccountCategory::Revenue,
93
+            AccountCategory::Expense,
94 94
         ])
95 95
             ->whereNotIn('type', [
96
-                AccountType::ContraAsset,
97 96
                 AccountType::ContraRevenue,
98 97
                 AccountType::ContraExpense,
99 98
                 AccountType::UncategorizedRevenue,

+ 6
- 6
composer.lock 查看文件

@@ -5997,16 +5997,16 @@
5997 5997
         },
5998 5998
         {
5999 5999
             "name": "psy/psysh",
6000
-            "version": "v0.12.7",
6000
+            "version": "v0.12.8",
6001 6001
             "source": {
6002 6002
                 "type": "git",
6003 6003
                 "url": "https://github.com/bobthecow/psysh.git",
6004
-                "reference": "d73fa3c74918ef4522bb8a3bf9cab39161c4b57c"
6004
+                "reference": "85057ceedee50c49d4f6ecaff73ee96adb3b3625"
6005 6005
             },
6006 6006
             "dist": {
6007 6007
                 "type": "zip",
6008
-                "url": "https://api.github.com/repos/bobthecow/psysh/zipball/d73fa3c74918ef4522bb8a3bf9cab39161c4b57c",
6009
-                "reference": "d73fa3c74918ef4522bb8a3bf9cab39161c4b57c",
6008
+                "url": "https://api.github.com/repos/bobthecow/psysh/zipball/85057ceedee50c49d4f6ecaff73ee96adb3b3625",
6009
+                "reference": "85057ceedee50c49d4f6ecaff73ee96adb3b3625",
6010 6010
                 "shasum": ""
6011 6011
             },
6012 6012
             "require": {
@@ -6070,9 +6070,9 @@
6070 6070
             ],
6071 6071
             "support": {
6072 6072
                 "issues": "https://github.com/bobthecow/psysh/issues",
6073
-                "source": "https://github.com/bobthecow/psysh/tree/v0.12.7"
6073
+                "source": "https://github.com/bobthecow/psysh/tree/v0.12.8"
6074 6074
             },
6075
-            "time": "2024-12-10T01:58:33+00:00"
6075
+            "time": "2025-03-16T03:05:19+00:00"
6076 6076
         },
6077 6077
         {
6078 6078
             "name": "ralouphie/getallheaders",

+ 1
- 1
database/migrations/2025_03_15_191245_create_budgets_table.php 查看文件

@@ -17,7 +17,7 @@ return new class extends Migration
17 17
             $table->string('name');
18 18
             $table->date('start_date');
19 19
             $table->date('end_date');
20
-            $table->string('status')->default('active'); // draft, active, closed
20
+            $table->string('status')->default('draft'); // draft, active, closed
21 21
             $table->string('interval_type')->default('month'); // day, week, month, quarter, year
22 22
             $table->text('notes')->nullable();
23 23
             $table->timestamp('approved_at')->nullable();

+ 6
- 6
package-lock.json 查看文件

@@ -1088,9 +1088,9 @@
1088 1088
             }
1089 1089
         },
1090 1090
         "node_modules/caniuse-lite": {
1091
-            "version": "1.0.30001704",
1092
-            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001704.tgz",
1093
-            "integrity": "sha512-+L2IgBbV6gXB4ETf0keSvLr7JUrRVbIaB/lrQ1+z8mRcQiisG5k+lG6O4n6Y5q6f5EuNfaYXKgymucphlEXQew==",
1091
+            "version": "1.0.30001705",
1092
+            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001705.tgz",
1093
+            "integrity": "sha512-S0uyMMiYvA7CxNgomYBwwwPUnWzFD83f3B1ce5jHUfHTH//QL6hHsreI8RVC5606R4ssqravelYO5TU6t8sEyg==",
1094 1094
             "dev": true,
1095 1095
             "funding": [
1096 1096
                 {
@@ -1930,9 +1930,9 @@
1930 1930
             }
1931 1931
         },
1932 1932
         "node_modules/nanoid": {
1933
-            "version": "3.3.9",
1934
-            "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.9.tgz",
1935
-            "integrity": "sha512-SppoicMGpZvbF1l3z4x7No3OlIjP7QJvC9XR7AhZr1kL133KHnKPztkKDc+Ir4aJ/1VhTySrtKhrsycmrMQfvg==",
1933
+            "version": "3.3.10",
1934
+            "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.10.tgz",
1935
+            "integrity": "sha512-vSJJTG+t/dIKAUhUDw/dLdZ9s//5OxcHqLaDWWrW4Cdq7o6tdLIczUkMXt2MBNmk6sJRZBZRXVixs7URY1CmIg==",
1936 1936
             "dev": true,
1937 1937
             "funding": [
1938 1938
                 {

Loading…
取消
儲存