1
|
|
|
<?php |
2
|
|
|
/** |
3
|
|
|
* BudgetController.php |
4
|
|
|
* Copyright (c) 2017 [email protected] |
5
|
|
|
* |
6
|
|
|
* This file is part of Firefly III. |
7
|
|
|
* |
8
|
|
|
* Firefly III is free software: you can redistribute it and/or modify |
9
|
|
|
* it under the terms of the GNU General Public License as published by |
10
|
|
|
* the Free Software Foundation, either version 3 of the License, or |
11
|
|
|
* (at your option) any later version. |
12
|
|
|
* |
13
|
|
|
* Firefly III is distributed in the hope that it will be useful, |
14
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
15
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
16
|
|
|
* GNU General Public License for more details. |
17
|
|
|
* |
18
|
|
|
* You should have received a copy of the GNU General Public License |
19
|
|
|
* along with Firefly III. If not, see <http://www.gnu.org/licenses/>. |
20
|
|
|
*/ |
21
|
|
|
declare(strict_types=1); |
22
|
|
|
|
23
|
|
|
namespace FireflyIII\Http\Controllers\Chart; |
24
|
|
|
|
25
|
|
|
use Carbon\Carbon; |
26
|
|
|
use FireflyIII\Exceptions\FireflyException; |
27
|
|
|
use FireflyIII\Generator\Chart\Basic\GeneratorInterface; |
28
|
|
|
use FireflyIII\Helpers\Collector\JournalCollectorInterface; |
29
|
|
|
use FireflyIII\Http\Controllers\Controller; |
30
|
|
|
use FireflyIII\Models\AccountType; |
31
|
|
|
use FireflyIII\Models\Budget; |
32
|
|
|
use FireflyIII\Models\BudgetLimit; |
33
|
|
|
use FireflyIII\Models\Transaction; |
34
|
|
|
use FireflyIII\Models\TransactionType; |
35
|
|
|
use FireflyIII\Repositories\Account\AccountRepositoryInterface; |
36
|
|
|
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; |
37
|
|
|
use FireflyIII\Repositories\Category\CategoryRepositoryInterface; |
38
|
|
|
use FireflyIII\Support\CacheProperties; |
39
|
|
|
use Illuminate\Support\Collection; |
40
|
|
|
use Steam; |
41
|
|
|
|
42
|
|
|
/** |
43
|
|
|
* Class BudgetController. |
44
|
|
|
* |
45
|
|
|
* @SuppressWarnings(PHPMD.CouplingBetweenObjects) // can't realy be helped. |
46
|
|
|
*/ |
47
|
|
|
class BudgetController extends Controller |
48
|
|
|
{ |
49
|
|
|
/** @var GeneratorInterface */ |
50
|
|
|
protected $generator; |
51
|
|
|
|
52
|
|
|
/** @var BudgetRepositoryInterface */ |
53
|
|
|
protected $repository; |
54
|
|
|
|
55
|
|
|
/** |
56
|
|
|
* BudgetController constructor. |
57
|
|
|
*/ |
58
|
|
|
public function __construct() |
59
|
|
|
{ |
60
|
|
|
parent::__construct(); |
61
|
|
|
|
62
|
|
|
$this->middleware( |
63
|
|
|
function ($request, $next) { |
64
|
|
|
$this->generator = app(GeneratorInterface::class); |
65
|
|
|
$this->repository = app(BudgetRepositoryInterface::class); |
66
|
|
|
|
67
|
|
|
return $next($request); |
68
|
|
|
} |
69
|
|
|
); |
70
|
|
|
} |
71
|
|
|
|
72
|
|
|
/** |
73
|
|
|
* @param Budget $budget |
74
|
|
|
* |
75
|
|
|
* @return \Symfony\Component\HttpFoundation\Response |
76
|
|
|
*/ |
77
|
|
|
public function budget(Budget $budget) |
78
|
|
|
{ |
79
|
|
|
$start = $this->repository->firstUseDate($budget); |
80
|
|
|
$end = session('end', new Carbon); |
81
|
|
|
$cache = new CacheProperties(); |
82
|
|
|
$cache->addProperty($start); |
83
|
|
|
$cache->addProperty($end); |
84
|
|
|
$cache->addProperty('chart.budget.budget'); |
85
|
|
|
$cache->addProperty($budget->id); |
86
|
|
|
|
87
|
|
|
if ($cache->has()) { |
88
|
|
|
return response()->json($cache->get()); // @codeCoverageIgnore |
89
|
|
|
} |
90
|
|
|
|
91
|
|
|
// depending on diff, do something with range of chart. |
92
|
|
|
$step = '1D'; |
93
|
|
|
$months = $start->diffInMonths($end); |
94
|
|
|
if ($months > 3) { |
95
|
|
|
$step = '1W'; |
96
|
|
|
} |
97
|
|
|
if ($months > 24) { |
98
|
|
|
$step = '1M'; |
99
|
|
|
} |
100
|
|
|
if ($months > 60) { |
101
|
|
|
$step = '1Y'; // @codeCoverageIgnore |
102
|
|
|
} |
103
|
|
|
$budgetCollection = new Collection([$budget]); |
104
|
|
|
$chartData = []; |
105
|
|
|
$current = clone $start; |
106
|
|
|
$current = app('navigation')->startOfPeriod($current, $step); |
|
|
|
|
107
|
|
|
|
108
|
|
|
while ($end >= $current) { |
109
|
|
|
$currentEnd = app('navigation')->endOfPeriod($current, $step); |
|
|
|
|
110
|
|
|
if ($step === '1Y') { |
111
|
|
|
$currentEnd->subDay(); // @codeCoverageIgnore |
112
|
|
|
} |
113
|
|
|
$spent = $this->repository->spentInPeriod($budgetCollection, new Collection, $current, $currentEnd); |
114
|
|
|
$label = app('navigation')->periodShow($current, $step); |
|
|
|
|
115
|
|
|
$chartData[$label] = (float)bcmul($spent, '-1'); |
116
|
|
|
$current = clone $currentEnd; |
117
|
|
|
$current->addDay(); |
118
|
|
|
} |
119
|
|
|
|
120
|
|
|
$data = $this->generator->singleSet((string)trans('firefly.spent'), $chartData); |
121
|
|
|
|
122
|
|
|
$cache->store($data); |
123
|
|
|
|
124
|
|
|
return response()->json($data); |
125
|
|
|
} |
126
|
|
|
|
127
|
|
|
/** |
128
|
|
|
* Shows the amount left in a specific budget limit. |
129
|
|
|
* |
130
|
|
|
* @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's exactly five. |
131
|
|
|
* |
132
|
|
|
* @param Budget $budget |
133
|
|
|
* @param BudgetLimit $budgetLimit |
134
|
|
|
* |
135
|
|
|
* @return \Symfony\Component\HttpFoundation\Response |
136
|
|
|
* |
137
|
|
|
* @throws FireflyException |
138
|
|
|
*/ |
139
|
|
|
public function budgetLimit(Budget $budget, BudgetLimit $budgetLimit) |
140
|
|
|
{ |
141
|
|
|
if ($budgetLimit->budget->id !== $budget->id) { |
142
|
|
|
throw new FireflyException('This budget limit is not part of this budget.'); |
143
|
|
|
} |
144
|
|
|
|
145
|
|
|
$start = clone $budgetLimit->start_date; |
|
|
|
|
146
|
|
|
$end = clone $budgetLimit->end_date; |
147
|
|
|
$cache = new CacheProperties(); |
148
|
|
|
$cache->addProperty($start); |
149
|
|
|
$cache->addProperty($end); |
150
|
|
|
$cache->addProperty('chart.budget.budget.limit'); |
151
|
|
|
$cache->addProperty($budgetLimit->id); |
152
|
|
|
$cache->addProperty($budget->id); |
153
|
|
|
|
154
|
|
|
if ($cache->has()) { |
155
|
|
|
return response()->json($cache->get()); // @codeCoverageIgnore |
156
|
|
|
} |
157
|
|
|
|
158
|
|
|
$entries = []; |
159
|
|
|
$amount = $budgetLimit->amount; |
|
|
|
|
160
|
|
|
$budgetCollection = new Collection([$budget]); |
161
|
|
|
while ($start <= $end) { |
162
|
|
|
$spent = $this->repository->spentInPeriod($budgetCollection, new Collection, $start, $start); |
163
|
|
|
$amount = bcadd($amount, $spent); |
164
|
|
|
$format = $start->formatLocalized((string)trans('config.month_and_day')); |
165
|
|
|
$entries[$format] = $amount; |
166
|
|
|
|
167
|
|
|
$start->addDay(); |
168
|
|
|
} |
169
|
|
|
$data = $this->generator->singleSet((string)trans('firefly.left'), $entries); |
170
|
|
|
$cache->store($data); |
171
|
|
|
|
172
|
|
|
return response()->json($data); |
173
|
|
|
} |
174
|
|
|
|
175
|
|
|
/** |
176
|
|
|
* @param Budget $budget |
177
|
|
|
* @param BudgetLimit|null $budgetLimit |
178
|
|
|
* |
179
|
|
|
* @return \Illuminate\Http\JsonResponse |
180
|
|
|
*/ |
181
|
|
|
public function expenseAsset(Budget $budget, ?BudgetLimit $budgetLimit) |
182
|
|
|
{ |
183
|
|
|
$cache = new CacheProperties; |
184
|
|
|
$cache->addProperty($budget->id); |
185
|
|
|
$cache->addProperty($budgetLimit->id ?? 0); |
186
|
|
|
$cache->addProperty('chart.budget.expense-asset'); |
187
|
|
|
if ($cache->has()) { |
188
|
|
|
return response()->json($cache->get()); // @codeCoverageIgnore |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
/** @var JournalCollectorInterface $collector */ |
192
|
|
|
$collector = app(JournalCollectorInterface::class); |
193
|
|
|
$collector->setAllAssetAccounts()->setBudget($budget); |
194
|
|
|
if (null !== $budgetLimit->id) { |
195
|
|
|
$collector->setRange($budgetLimit->start_date, $budgetLimit->end_date); |
|
|
|
|
196
|
|
|
} |
197
|
|
|
|
198
|
|
|
$transactions = $collector->getJournals(); |
199
|
|
|
$result = []; |
200
|
|
|
$chartData = []; |
201
|
|
|
/** @var Transaction $transaction */ |
202
|
|
|
foreach ($transactions as $transaction) { |
203
|
|
|
$assetId = (int)$transaction->account_id; |
204
|
|
|
$result[$assetId] = $result[$assetId] ?? '0'; |
205
|
|
|
$result[$assetId] = bcadd($transaction->transaction_amount, $result[$assetId]); |
206
|
|
|
} |
207
|
|
|
|
208
|
|
|
$names = $this->getAccountNames(array_keys($result)); |
209
|
|
|
foreach ($result as $assetId => $amount) { |
210
|
|
|
$chartData[$names[$assetId]] = $amount; |
211
|
|
|
} |
212
|
|
|
|
213
|
|
|
$data = $this->generator->pieChart($chartData); |
214
|
|
|
$cache->store($data); |
215
|
|
|
|
216
|
|
|
return response()->json($data); |
217
|
|
|
} |
218
|
|
|
|
219
|
|
|
/** |
220
|
|
|
* @param Budget $budget |
221
|
|
|
* @param BudgetLimit|null $budgetLimit |
222
|
|
|
* |
223
|
|
|
* @return \Illuminate\Http\JsonResponse |
224
|
|
|
*/ |
225
|
|
|
public function expenseCategory(Budget $budget, ?BudgetLimit $budgetLimit) |
226
|
|
|
{ |
227
|
|
|
$cache = new CacheProperties; |
228
|
|
|
$cache->addProperty($budget->id); |
229
|
|
|
$cache->addProperty($budgetLimit->id ?? 0); |
230
|
|
|
$cache->addProperty('chart.budget.expense-category'); |
231
|
|
|
if ($cache->has()) { |
232
|
|
|
return response()->json($cache->get()); // @codeCoverageIgnore |
233
|
|
|
} |
234
|
|
|
|
235
|
|
|
/** @var JournalCollectorInterface $collector */ |
236
|
|
|
$collector = app(JournalCollectorInterface::class); |
237
|
|
|
$collector->setAllAssetAccounts()->setBudget($budget)->withCategoryInformation(); |
238
|
|
|
if (null !== $budgetLimit->id) { |
239
|
|
|
$collector->setRange($budgetLimit->start_date, $budgetLimit->end_date); |
|
|
|
|
240
|
|
|
} |
241
|
|
|
|
242
|
|
|
$transactions = $collector->getJournals(); |
243
|
|
|
$result = []; |
244
|
|
|
$chartData = []; |
245
|
|
|
/** @var Transaction $transaction */ |
246
|
|
|
foreach ($transactions as $transaction) { |
247
|
|
|
$jrnlCatId = (int)$transaction->transaction_journal_category_id; |
248
|
|
|
$transCatId = (int)$transaction->transaction_category_id; |
249
|
|
|
$categoryId = max($jrnlCatId, $transCatId); |
250
|
|
|
$result[$categoryId] = $result[$categoryId] ?? '0'; |
251
|
|
|
$result[$categoryId] = bcadd($transaction->transaction_amount, $result[$categoryId]); |
252
|
|
|
} |
253
|
|
|
|
254
|
|
|
$names = $this->getCategoryNames(array_keys($result)); |
255
|
|
|
foreach ($result as $categoryId => $amount) { |
256
|
|
|
$chartData[$names[$categoryId]] = $amount; |
257
|
|
|
} |
258
|
|
|
|
259
|
|
|
$data = $this->generator->pieChart($chartData); |
260
|
|
|
$cache->store($data); |
261
|
|
|
|
262
|
|
|
return response()->json($data); |
263
|
|
|
} |
264
|
|
|
|
265
|
|
|
/** |
266
|
|
|
* @param Budget $budget |
267
|
|
|
* @param BudgetLimit|null $budgetLimit |
268
|
|
|
* |
269
|
|
|
* @return \Illuminate\Http\JsonResponse |
270
|
|
|
*/ |
271
|
|
|
public function expenseExpense(Budget $budget, ?BudgetLimit $budgetLimit) |
272
|
|
|
{ |
273
|
|
|
$cache = new CacheProperties; |
274
|
|
|
$cache->addProperty($budget->id); |
275
|
|
|
$cache->addProperty($budgetLimit->id ?? 0); |
276
|
|
|
$cache->addProperty('chart.budget.expense-expense'); |
277
|
|
|
if ($cache->has()) { |
278
|
|
|
return response()->json($cache->get()); // @codeCoverageIgnore |
279
|
|
|
} |
280
|
|
|
|
281
|
|
|
/** @var JournalCollectorInterface $collector */ |
282
|
|
|
$collector = app(JournalCollectorInterface::class); |
283
|
|
|
$collector->setAllAssetAccounts()->setTypes([TransactionType::WITHDRAWAL])->setBudget($budget)->withOpposingAccount(); |
284
|
|
|
if (null !== $budgetLimit->id) { |
285
|
|
|
$collector->setRange($budgetLimit->start_date, $budgetLimit->end_date); |
|
|
|
|
286
|
|
|
} |
287
|
|
|
|
288
|
|
|
$transactions = $collector->getJournals(); |
289
|
|
|
$result = []; |
290
|
|
|
$chartData = []; |
291
|
|
|
/** @var Transaction $transaction */ |
292
|
|
|
foreach ($transactions as $transaction) { |
293
|
|
|
$opposingId = (int)$transaction->opposing_account_id; |
294
|
|
|
$result[$opposingId] = $result[$opposingId] ?? '0'; |
295
|
|
|
$result[$opposingId] = bcadd($transaction->transaction_amount, $result[$opposingId]); |
296
|
|
|
} |
297
|
|
|
|
298
|
|
|
$names = $this->getAccountNames(array_keys($result)); |
299
|
|
|
foreach ($result as $opposingId => $amount) { |
300
|
|
|
$name = $names[$opposingId] ?? 'no name'; |
301
|
|
|
$chartData[$name] = $amount; |
302
|
|
|
} |
303
|
|
|
|
304
|
|
|
$data = $this->generator->pieChart($chartData); |
305
|
|
|
$cache->store($data); |
306
|
|
|
|
307
|
|
|
return response()->json($data); |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
/** |
311
|
|
|
* Shows a budget list with spent/left/overspent. |
312
|
|
|
* |
313
|
|
|
* @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's exactly five. |
314
|
|
|
* @SuppressWarnings(PHPMD.ExcessiveMethodLength) // 46 lines, I'm fine with this. |
315
|
|
|
* |
316
|
|
|
* @return \Symfony\Component\HttpFoundation\Response |
317
|
|
|
*/ |
318
|
|
|
public function frontpage() |
319
|
|
|
{ |
320
|
|
|
$start = session('start', Carbon::now()->startOfMonth()); |
321
|
|
|
$end = session('end', Carbon::now()->endOfMonth()); |
322
|
|
|
// chart properties for cache: |
323
|
|
|
$cache = new CacheProperties(); |
324
|
|
|
$cache->addProperty($start); |
325
|
|
|
$cache->addProperty($end); |
326
|
|
|
$cache->addProperty('chart.budget.frontpage'); |
327
|
|
|
if ($cache->has()) { |
328
|
|
|
return response()->json($cache->get()); // @codeCoverageIgnore |
329
|
|
|
} |
330
|
|
|
$budgets = $this->repository->getActiveBudgets(); |
331
|
|
|
$chartData = [ |
332
|
|
|
['label' => (string)trans('firefly.spent_in_budget'), 'entries' => [], 'type' => 'bar'], |
333
|
|
|
['label' => (string)trans('firefly.left_to_spend'), 'entries' => [], 'type' => 'bar'], |
334
|
|
|
['label' => (string)trans('firefly.overspent'), 'entries' => [], 'type' => 'bar'], |
335
|
|
|
]; |
336
|
|
|
|
337
|
|
|
/** @var Budget $budget */ |
338
|
|
|
foreach ($budgets as $budget) { |
339
|
|
|
// get relevant repetitions: |
340
|
|
|
$limits = $this->repository->getBudgetLimits($budget, $start, $end); |
|
|
|
|
341
|
|
|
$expenses = $this->getExpensesForBudget($limits, $budget, $start, $end); |
|
|
|
|
342
|
|
|
|
343
|
|
|
foreach ($expenses as $name => $row) { |
344
|
|
|
$chartData[0]['entries'][$name] = $row['spent']; |
345
|
|
|
$chartData[1]['entries'][$name] = $row['left']; |
346
|
|
|
$chartData[2]['entries'][$name] = $row['overspent']; |
347
|
|
|
} |
348
|
|
|
} |
349
|
|
|
// for no budget: |
350
|
|
|
$spent = $this->spentInPeriodWithout($start, $end); |
|
|
|
|
351
|
|
|
$name = (string)trans('firefly.no_budget'); |
352
|
|
|
if (0 !== bccomp($spent, '0')) { |
353
|
|
|
$chartData[0]['entries'][$name] = bcmul($spent, '-1'); |
354
|
|
|
$chartData[1]['entries'][$name] = '0'; |
355
|
|
|
$chartData[2]['entries'][$name] = '0'; |
356
|
|
|
} |
357
|
|
|
|
358
|
|
|
$data = $this->generator->multiSet($chartData); |
359
|
|
|
$cache->store($data); |
360
|
|
|
|
361
|
|
|
return response()->json($data); |
362
|
|
|
} |
363
|
|
|
|
364
|
|
|
/** |
365
|
|
|
* @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's exactly five. |
366
|
|
|
* |
367
|
|
|
* @param Budget $budget |
368
|
|
|
* @param Carbon $start |
369
|
|
|
* @param Carbon $end |
370
|
|
|
* @param Collection $accounts |
371
|
|
|
* |
372
|
|
|
* @return \Illuminate\Http\JsonResponse |
373
|
|
|
*/ |
374
|
|
|
public function period(Budget $budget, Collection $accounts, Carbon $start, Carbon $end) |
375
|
|
|
{ |
376
|
|
|
// chart properties for cache: |
377
|
|
|
$cache = new CacheProperties(); |
378
|
|
|
$cache->addProperty($start); |
379
|
|
|
$cache->addProperty($end); |
380
|
|
|
$cache->addProperty($accounts); |
381
|
|
|
$cache->addProperty($budget->id); |
382
|
|
|
$cache->addProperty('chart.budget.period'); |
383
|
|
|
if ($cache->has()) { |
384
|
|
|
return response()->json($cache->get()); // @codeCoverageIgnore |
385
|
|
|
} |
386
|
|
|
$periods = app('navigation')->listOfPeriods($start, $end); |
|
|
|
|
387
|
|
|
$entries = $this->repository->getBudgetPeriodReport(new Collection([$budget]), $accounts, $start, $end); // get the expenses |
388
|
|
|
$budgeted = $this->getBudgetedInPeriod($budget, $start, $end); |
389
|
|
|
|
390
|
|
|
// join them into one set of data: |
391
|
|
|
$chartData = [ |
392
|
|
|
['label' => (string)trans('firefly.spent'), 'type' => 'bar', 'entries' => []], |
393
|
|
|
['label' => (string)trans('firefly.budgeted'), 'type' => 'bar', 'entries' => []], |
394
|
|
|
]; |
395
|
|
|
|
396
|
|
|
foreach (array_keys($periods) as $period) { |
397
|
|
|
$label = $periods[$period]; |
398
|
|
|
$spent = $entries[$budget->id]['entries'][$period] ?? '0'; |
399
|
|
|
$limit = (int)($budgeted[$period] ?? 0); |
400
|
|
|
$chartData[0]['entries'][$label] = round(bcmul($spent, '-1'), 12); |
|
|
|
|
401
|
|
|
$chartData[1]['entries'][$label] = $limit; |
402
|
|
|
} |
403
|
|
|
$data = $this->generator->multiSet($chartData); |
404
|
|
|
$cache->store($data); |
405
|
|
|
|
406
|
|
|
return response()->json($data); |
407
|
|
|
} |
408
|
|
|
|
409
|
|
|
/** |
410
|
|
|
* @param Collection $accounts |
411
|
|
|
* @param Carbon $start |
412
|
|
|
* @param Carbon $end |
413
|
|
|
* |
414
|
|
|
* @return \Illuminate\Http\JsonResponse |
415
|
|
|
*/ |
416
|
|
|
public function periodNoBudget(Collection $accounts, Carbon $start, Carbon $end) |
417
|
|
|
{ |
418
|
|
|
// chart properties for cache: |
419
|
|
|
$cache = new CacheProperties(); |
420
|
|
|
$cache->addProperty($start); |
421
|
|
|
$cache->addProperty($end); |
422
|
|
|
$cache->addProperty($accounts); |
423
|
|
|
$cache->addProperty('chart.budget.no-budget'); |
424
|
|
|
if ($cache->has()) { |
425
|
|
|
return response()->json($cache->get()); // @codeCoverageIgnore |
426
|
|
|
} |
427
|
|
|
|
428
|
|
|
// the expenses: |
429
|
|
|
$periods = app('navigation')->listOfPeriods($start, $end); |
430
|
|
|
$entries = $this->repository->getNoBudgetPeriodReport($accounts, $start, $end); |
431
|
|
|
$chartData = []; |
432
|
|
|
|
433
|
|
|
// join them: |
434
|
|
|
foreach (array_keys($periods) as $period) { |
435
|
|
|
$label = $periods[$period]; |
436
|
|
|
$spent = $entries['entries'][$period] ?? '0'; |
437
|
|
|
$chartData[$label] = bcmul($spent, '-1'); |
438
|
|
|
} |
439
|
|
|
$data = $this->generator->singleSet((string)trans('firefly.spent'), $chartData); |
440
|
|
|
$cache->store($data); |
441
|
|
|
|
442
|
|
|
return response()->json($data); |
443
|
|
|
} |
444
|
|
|
|
445
|
|
|
/** |
446
|
|
|
* @param array $accountIds |
447
|
|
|
* |
448
|
|
|
* @return array |
449
|
|
|
*/ |
450
|
|
|
private function getAccountNames(array $accountIds): array |
451
|
|
|
{ |
452
|
|
|
/** @var AccountRepositoryInterface $repository */ |
453
|
|
|
$repository = app(AccountRepositoryInterface::class); |
454
|
|
|
$accounts = $repository->getAccountsByType([AccountType::ASSET, AccountType::DEFAULT, AccountType::EXPENSE, AccountType::CASH]); |
455
|
|
|
$grouped = $accounts->groupBy('id')->toArray(); |
456
|
|
|
$return = []; |
457
|
|
|
foreach ($accountIds as $accountId) { |
458
|
|
|
if (isset($grouped[$accountId])) { |
459
|
|
|
$return[$accountId] = $grouped[$accountId][0]['name']; |
460
|
|
|
} |
461
|
|
|
} |
462
|
|
|
$return[0] = '(no name)'; |
463
|
|
|
|
464
|
|
|
return $return; |
465
|
|
|
} |
466
|
|
|
|
467
|
|
|
/** |
468
|
|
|
* @param Budget $budget |
469
|
|
|
* @param Carbon $start |
470
|
|
|
* @param Carbon $end |
471
|
|
|
* |
472
|
|
|
* @return array |
473
|
|
|
*/ |
474
|
|
|
private function getBudgetedInPeriod(Budget $budget, Carbon $start, Carbon $end): array |
475
|
|
|
{ |
476
|
|
|
$key = app('navigation')->preferredCarbonFormat($start, $end); |
|
|
|
|
477
|
|
|
$range = app('navigation')->preferredRangeFormat($start, $end); |
|
|
|
|
478
|
|
|
$current = clone $start; |
479
|
|
|
$budgeted = []; |
480
|
|
|
while ($current < $end) { |
481
|
|
|
$currentStart = app('navigation')->startOfPeriod($current, $range); |
482
|
|
|
$currentEnd = app('navigation')->endOfPeriod($current, $range); |
483
|
|
|
$budgetLimits = $this->repository->getBudgetLimits($budget, $currentStart, $currentEnd); |
484
|
|
|
$index = $currentStart->format($key); |
485
|
|
|
$budgeted[$index] = $budgetLimits->sum('amount'); |
486
|
|
|
$currentEnd->addDay(); |
487
|
|
|
$current = clone $currentEnd; |
488
|
|
|
} |
489
|
|
|
|
490
|
|
|
return $budgeted; |
491
|
|
|
} |
492
|
|
|
|
493
|
|
|
/** |
494
|
|
|
* Small helper function for some of the charts. |
495
|
|
|
* |
496
|
|
|
* @param array $categoryIds |
497
|
|
|
* |
498
|
|
|
* @return array |
499
|
|
|
*/ |
500
|
|
|
private function getCategoryNames(array $categoryIds): array |
501
|
|
|
{ |
502
|
|
|
/** @var CategoryRepositoryInterface $repository */ |
503
|
|
|
$repository = app(CategoryRepositoryInterface::class); |
504
|
|
|
$categories = $repository->getCategories(); |
505
|
|
|
$grouped = $categories->groupBy('id')->toArray(); |
506
|
|
|
$return = []; |
507
|
|
|
foreach ($categoryIds as $categoryId) { |
508
|
|
|
if (isset($grouped[$categoryId])) { |
509
|
|
|
$return[$categoryId] = $grouped[$categoryId][0]['name']; |
510
|
|
|
} |
511
|
|
|
} |
512
|
|
|
$return[0] = trans('firefly.noCategory'); |
513
|
|
|
|
514
|
|
|
return $return; |
515
|
|
|
} |
516
|
|
|
|
517
|
|
|
/** |
518
|
|
|
* @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's 6 but ok. |
519
|
|
|
* |
520
|
|
|
* @param Collection $limits |
521
|
|
|
* @param Budget $budget |
522
|
|
|
* @param Carbon $start |
523
|
|
|
* @param Carbon $end |
524
|
|
|
* |
525
|
|
|
* @return array |
526
|
|
|
*/ |
527
|
|
|
private function getExpensesForBudget(Collection $limits, Budget $budget, Carbon $start, Carbon $end): array |
528
|
|
|
{ |
529
|
|
|
$return = []; |
530
|
|
|
if (0 === $limits->count()) { |
531
|
|
|
$spent = $this->repository->spentInPeriod(new Collection([$budget]), new Collection, $start, $end); |
532
|
|
|
if (0 !== bccomp($spent, '0')) { |
533
|
|
|
$return[$budget->name]['spent'] = bcmul($spent, '-1'); |
534
|
|
|
$return[$budget->name]['left'] = 0; |
535
|
|
|
$return[$budget->name]['overspent'] = 0; |
536
|
|
|
} |
537
|
|
|
|
538
|
|
|
return $return; |
539
|
|
|
} |
540
|
|
|
|
541
|
|
|
$rows = $this->spentInPeriodMulti($budget, $limits); |
542
|
|
|
foreach ($rows as $name => $row) { |
543
|
|
|
if (0 !== bccomp($row['spent'], '0') || 0 !== bccomp($row['left'], '0')) { |
544
|
|
|
$return[$name] = $row; |
545
|
|
|
} |
546
|
|
|
} |
547
|
|
|
unset($rows, $row); |
548
|
|
|
|
549
|
|
|
return $return; |
550
|
|
|
} |
551
|
|
|
|
552
|
|
|
/** |
553
|
|
|
* @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's exactly five. |
554
|
|
|
* |
555
|
|
|
* Returns an array with the following values: |
556
|
|
|
* 0 => |
557
|
|
|
* 'name' => name of budget + repetition |
558
|
|
|
* 'left' => left in budget repetition (always zero) |
559
|
|
|
* 'overspent' => spent more than budget repetition? (always zero) |
560
|
|
|
* 'spent' => actually spent in period for budget |
561
|
|
|
* 1 => (etc) |
562
|
|
|
* |
563
|
|
|
* @param Budget $budget |
564
|
|
|
* @param Collection $limits |
565
|
|
|
* |
566
|
|
|
* @return array |
567
|
|
|
*/ |
568
|
|
|
private function spentInPeriodMulti(Budget $budget, Collection $limits): array |
569
|
|
|
{ |
570
|
|
|
$return = []; |
571
|
|
|
$format = (string)trans('config.month_and_day'); |
572
|
|
|
$name = $budget->name; |
573
|
|
|
/** @var BudgetLimit $budgetLimit */ |
574
|
|
|
foreach ($limits as $budgetLimit) { |
575
|
|
|
$expenses = $this->repository->spentInPeriod(new Collection([$budget]), new Collection, $budgetLimit->start_date, $budgetLimit->end_date); |
576
|
|
|
$expenses = Steam::positive($expenses); |
|
|
|
|
577
|
|
|
|
578
|
|
|
if ($limits->count() > 1) { |
579
|
|
|
$name = $budget->name . ' ' . trans( |
|
|
|
|
580
|
|
|
'firefly.between_dates', |
581
|
|
|
[ |
582
|
|
|
'start' => $budgetLimit->start_date->formatLocalized($format), |
583
|
|
|
'end' => $budgetLimit->end_date->formatLocalized($format), |
584
|
|
|
] |
585
|
|
|
); |
586
|
|
|
} |
587
|
|
|
/* |
588
|
|
|
* amount: amount of budget limit |
589
|
|
|
* left: amount of budget limit min spent, or 0 when < 0. |
590
|
|
|
* spent: spent, or amount of budget limit when > amount |
591
|
|
|
*/ |
592
|
|
|
$amount = $budgetLimit->amount; |
593
|
|
|
$leftInLimit = bcsub($amount, $expenses); |
594
|
|
|
$hasOverspent = bccomp($leftInLimit, '0') === -1; |
595
|
|
|
|
596
|
|
|
$left = $hasOverspent ? '0' : bcsub($amount, $expenses); |
597
|
|
|
$spent = $hasOverspent ? $amount : $expenses; |
598
|
|
|
$overspent = $hasOverspent ? Steam::positive($leftInLimit) : '0'; |
599
|
|
|
|
600
|
|
|
$return[$name] = [ |
601
|
|
|
'left' => $left, |
602
|
|
|
'overspent' => $overspent, |
603
|
|
|
'spent' => $spent, |
604
|
|
|
]; |
605
|
|
|
} |
606
|
|
|
|
607
|
|
|
return $return; |
608
|
|
|
} |
609
|
|
|
|
610
|
|
|
/** |
611
|
|
|
* Returns an array with the following values: |
612
|
|
|
* 'name' => "no budget" in local language |
613
|
|
|
* 'repetition_left' => left in budget repetition (always zero) |
614
|
|
|
* 'repetition_overspent' => spent more than budget repetition? (always zero) |
615
|
|
|
* 'spent' => actually spent in period for budget. |
616
|
|
|
* |
617
|
|
|
* @param Carbon $start |
618
|
|
|
* @param Carbon $end |
619
|
|
|
* |
620
|
|
|
* @return string |
621
|
|
|
*/ |
622
|
|
|
private function spentInPeriodWithout(Carbon $start, Carbon $end): string |
623
|
|
|
{ |
624
|
|
|
// collector |
625
|
|
|
/** @var JournalCollectorInterface $collector */ |
626
|
|
|
$collector = app(JournalCollectorInterface::class); |
627
|
|
|
$types = [TransactionType::WITHDRAWAL]; |
628
|
|
|
$collector->setAllAssetAccounts()->setTypes($types)->setRange($start, $end)->withoutBudget(); |
629
|
|
|
$journals = $collector->getJournals(); |
630
|
|
|
$sum = '0'; |
631
|
|
|
/** @var Transaction $entry */ |
632
|
|
|
foreach ($journals as $entry) { |
633
|
|
|
$sum = bcadd($entry->transaction_amount, $sum); |
634
|
|
|
} |
635
|
|
|
|
636
|
|
|
return $sum; |
637
|
|
|
} |
638
|
|
|
} |
639
|
|
|
|
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.
This is most likely a typographical error or the method has been renamed.