From c916fbbee934b25ffbdccadef46037d9c00e2525 Mon Sep 17 00:00:00 2001 From: James Cole Date: Mon, 6 Jun 2022 16:41:54 +0200 Subject: [PATCH] Add code for budgets --- app/Api/V2/Controllers/Controller.php | 56 ++++++ .../Model/Budget/SumController.php | 13 ++ .../System/PreferencesController.php | 46 +++++ app/Repositories/Budget/BudgetRepository.php | 173 ++++++++++++------ .../Budget/BudgetRepositoryInterface.php | 10 + .../Budget/OperationsRepository.php | 2 +- .../Budget/OperationsRepositoryInterface.php | 2 + frontend/src/api/v2/budgets/sum.js | 12 +- .../components/dashboard/SpendInsightBox.vue | 26 ++- routes/api.php | 4 +- 10 files changed, 280 insertions(+), 64 deletions(-) create mode 100644 app/Api/V2/Controllers/Controller.php create mode 100644 app/Api/V2/Controllers/System/PreferencesController.php diff --git a/app/Api/V2/Controllers/Controller.php b/app/Api/V2/Controllers/Controller.php new file mode 100644 index 0000000000..8d8ab59805 --- /dev/null +++ b/app/Api/V2/Controllers/Controller.php @@ -0,0 +1,56 @@ +. + */ + +namespace FireflyIII\Api\V2\Controllers; + +use FireflyIII\Transformers\AbstractTransformer; +use Illuminate\Database\Eloquent\Model; +use Illuminate\Routing\Controller as BaseController; +use League\Fractal\Manager; +use League\Fractal\Resource\Item; +use League\Fractal\Serializer\JsonApiSerializer; + +/** + * Class Controller + */ +class Controller extends BaseController +{ + protected const CONTENT_TYPE = 'application/vnd.api+json'; + /** + * Returns a JSON API object and returns it. + * + * @param string $key + * @param Model $object + * @param AbstractTransformer $transformer + * @return array + */ + final protected function jsonApiObject(string $key, Model $object, AbstractTransformer $transformer): array + { + // create some objects: + $manager = new Manager; + $baseUrl = request()->getSchemeAndHttpHost() . '/api/v2'; + $manager->setSerializer(new JsonApiSerializer($baseUrl)); + + $resource = new Item($object, $transformer, $key); + return $manager->createData($resource)->toArray(); + } + +} diff --git a/app/Api/V2/Controllers/Model/Budget/SumController.php b/app/Api/V2/Controllers/Model/Budget/SumController.php index c4b524b87d..2d53bca500 100644 --- a/app/Api/V2/Controllers/Model/Budget/SumController.php +++ b/app/Api/V2/Controllers/Model/Budget/SumController.php @@ -62,4 +62,17 @@ class SumController extends Controller return response()->json($converted); } + /** + * @param DateRequest $request + * @return JsonResponse + */ + public function spent(DateRequest $request): JsonResponse + { + $data = $request->getAll(); + $result = $this->repository->spentInPeriod($data['start'], $data['end']); + $converted = $this->cerSum(array_values($result)); + + return response()->json($converted); + } + } diff --git a/app/Api/V2/Controllers/System/PreferencesController.php b/app/Api/V2/Controllers/System/PreferencesController.php new file mode 100644 index 0000000000..acead47381 --- /dev/null +++ b/app/Api/V2/Controllers/System/PreferencesController.php @@ -0,0 +1,46 @@ +. + */ + +namespace FireflyIII\Api\V2\Controllers\System; + +use FireflyIII\Api\V2\Controllers\Controller; +use FireflyIII\Models\Preference; +use FireflyIII\Transformers\PreferenceTransformer; +use Illuminate\Http\JsonResponse; + +/** + * Class PreferencesController + */ +class PreferencesController extends Controller +{ + + /** + * @param Preference $preference + * @return JsonResponse + */ + public function get(Preference $preference): JsonResponse + { + return response() + ->json($this->jsonApiObject('preferences', $preference, new PreferenceTransformer)) + ->header('Content-Type', self::CONTENT_TYPE); + } + +} diff --git a/app/Repositories/Budget/BudgetRepository.php b/app/Repositories/Budget/BudgetRepository.php index 855c3aa60a..f9f6d97217 100644 --- a/app/Repositories/Budget/BudgetRepository.php +++ b/app/Repositories/Budget/BudgetRepository.php @@ -26,6 +26,8 @@ use Carbon\Carbon; use DB; use Exception; use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Helpers\Collector\GroupCollectorInterface; +use FireflyIII\Models\Account; use FireflyIII\Models\Attachment; use FireflyIII\Models\AutoBudget; use FireflyIII\Models\Budget; @@ -34,6 +36,8 @@ use FireflyIII\Models\Note; use FireflyIII\Models\RecurrenceTransactionMeta; use FireflyIII\Models\RuleAction; use FireflyIII\Models\RuleTrigger; +use FireflyIII\Models\TransactionType; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; use FireflyIII\Services\Internal\Destroy\BudgetDestroyService; use FireflyIII\User; @@ -126,15 +130,64 @@ class BudgetRepository implements BudgetRepositoryInterface $return[$currency->id]['sum'] = bcadd($return[$currency->id]['sum'], $amount); Log::debug(sprintf('Amount per day: %s (%s over %d days). Total amount for %d days: %s', bcdiv((string) $limit->amount, (string) $total), - $limit->amount, - $total, - $days, - $amount)); + $limit->amount, + $total, + $days, + $amount)); } } return $return; } + /** + * @return Collection + */ + public function getActiveBudgets(): Collection + { + return $this->user->budgets()->where('active', true) + ->orderBy('order', 'ASC') + ->orderBy('name', 'ASC') + ->get(); + } + + /** + * How many days of this budget limit are between start and end? + * + * @param BudgetLimit $limit + * @param Carbon $start + * @param Carbon $end + * @return int + */ + private function daysInOverlap(BudgetLimit $limit, Carbon $start, Carbon $end): int + { + // start1 = $start + // start2 = $limit->start_date + // start1 = $end + // start2 = $limit->end_date + + // limit is larger than start and end (inclusive) + // |-----------| + // |----------------| + if ($start->gte($limit->start_date) && $end->lte($limit->end_date)) { + return $start->diffInDays($end) + 1; // add one day + } + // limit starts earlier and limit ends first: + // |-----------| + // |-------| + if ($limit->start_date->lte($start) && $limit->end_date->lte($end)) { + // return days in the range $start-$limit_end + return $start->diffInDays($limit->end_date) + 1; // add one day, the day itself + } + // limit starts later and limit ends earlier + // |-----------| + // |-------| + if ($limit->start_date->gte($start) && $limit->end_date->gte($end)) { + // return days in the range $limit_start - $end + return $limit->start_date->diffInDays($end) + 1; // add one day, the day itself + } + return 0; + } + /** * @return bool */ @@ -161,17 +214,6 @@ class BudgetRepository implements BudgetRepositoryInterface return true; } - /** - * @return Collection - */ - public function getActiveBudgets(): Collection - { - return $this->user->budgets()->where('active', true) - ->orderBy('order', 'ASC') - ->orderBy('name', 'ASC') - ->get(); - } - /** * @param Budget $budget * @@ -385,6 +427,69 @@ class BudgetRepository implements BudgetRepositoryInterface $this->user = $user; } + /** + * @inheritDoc + */ + public function spentInPeriod(Carbon $start, Carbon $end): array + { + Log::debug(sprintf('Now in %s', __METHOD__)); + $start->startOfDay(); + $end->endOfDay(); + + // exclude specific liabilities + $repository = app(AccountRepositoryInterface::class); + $repository->setUser($this->user); + $subset = $repository->getAccountsByType(config('firefly.valid_liabilities')); + $selection = new Collection; + /** @var Account $account */ + foreach ($subset as $account) { + if ('credit' === $repository->getMetaValue($account, 'liability_direction')) { + $selection->push($account); + } + } + + // start collecting: + /** @var GroupCollectorInterface $collector */ + $collector = app(GroupCollectorInterface::class); + $collector->setUser($this->user) + ->setRange($start, $end) + ->excludeDestinationAccounts($selection) + ->setTypes([TransactionType::WITHDRAWAL]) + ->setBudgets($this->getActiveBudgets()); + + $journals = $collector->getExtractedJournals(); + $array = []; + + foreach ($journals as $journal) { + $currencyId = (int) $journal['currency_id']; + $array[$currencyId] = $array[$currencyId] ?? [ + 'id' => (string) $currencyId, + 'name' => $journal['currency_name'], + 'symbol' => $journal['currency_symbol'], + 'code' => $journal['currency_code'], + 'decimal_places' => $journal['currency_decimal_places'], + 'sum' => '0', + ]; + $array[$currencyId]['sum'] = bcadd($array[$currencyId]['sum'], app('steam')->negative($journal['amount'])); + + // also do foreign amount: + $foreignId = (int) $journal['foreign_currency_id']; + if (0 !== $foreignId) { + $array[$foreignId] = $array[$foreignId] ?? [ + 'id' => (string) $foreignId, + 'name' => $journal['foreign_currency_name'], + 'symbol' => $journal['foreign_currency_symbol'], + 'code' => $journal['foreign_currency_code'], + 'decimal_places' => $journal['foreign_currency_decimal_places'], + 'sum' => '0', + ]; + $array[$foreignId]['sum'] = bcadd($array[$foreignId]['sum'], app('steam')->negative($journal['foreign_amount'])); + } + } + + return $array; + } + /** * @param array $data * @@ -652,42 +757,4 @@ class BudgetRepository implements BudgetRepositoryInterface $autoBudget->save(); } - - /** - * How many days of this budget limit are between start and end? - * - * @param BudgetLimit $limit - * @param Carbon $start - * @param Carbon $end - * @return int - */ - private function daysInOverlap(BudgetLimit $limit, Carbon $start, Carbon $end): int - { - // start1 = $start - // start2 = $limit->start_date - // start1 = $end - // start2 = $limit->end_date - - // limit is larger than start and end (inclusive) - // |-----------| - // |----------------| - if ($start->gte($limit->start_date) && $end->lte($limit->end_date)) { - return $start->diffInDays($end) + 1; // add one day - } - // limit starts earlier and limit ends first: - // |-----------| - // |-------| - if ($limit->start_date->lte($start) && $limit->end_date->lte($end)) { - // return days in the range $start-$limit_end - return $start->diffInDays($limit->end_date) + 1; // add one day, the day itself - } - // limit starts later and limit ends earlier - // |-----------| - // |-------| - if ($limit->start_date->gte($start) && $limit->end_date->gte($end)) { - // return days in the range $limit_start - $end - return $limit->start_date->diffInDays($end) + 1; // add one day, the day itself - } - return 0; - } } diff --git a/app/Repositories/Budget/BudgetRepositoryInterface.php b/app/Repositories/Budget/BudgetRepositoryInterface.php index 912351d704..4a41a5b28d 100644 --- a/app/Repositories/Budget/BudgetRepositoryInterface.php +++ b/app/Repositories/Budget/BudgetRepositoryInterface.php @@ -185,6 +185,16 @@ interface BudgetRepositoryInterface */ public function setUser(User $user); + /** + * Used in the v2 API to calculate the amount of money spent in all active budgets. + * + * @param Carbon $start + * @param Carbon $end + * + * @return array + */ + public function spentInPeriod(Carbon $start, Carbon $end): array; + /** * @param array $data * diff --git a/app/Repositories/Budget/OperationsRepository.php b/app/Repositories/Budget/OperationsRepository.php index cf2777b26d..d1f20a8b6d 100644 --- a/app/Repositories/Budget/OperationsRepository.php +++ b/app/Repositories/Budget/OperationsRepository.php @@ -288,7 +288,7 @@ class OperationsRepository implements OperationsRepositoryInterface * @param Collection|null $accounts * @param Collection|null $budgets * @param TransactionCurrency|null $currency - * + * @deprecated * @return array */ public function sumExpenses(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $budgets = null, ?TransactionCurrency $currency = null diff --git a/app/Repositories/Budget/OperationsRepositoryInterface.php b/app/Repositories/Budget/OperationsRepositoryInterface.php index 691ff64805..016e287445 100644 --- a/app/Repositories/Budget/OperationsRepositoryInterface.php +++ b/app/Repositories/Budget/OperationsRepositoryInterface.php @@ -90,6 +90,7 @@ interface OperationsRepositoryInterface public function spentInPeriodMc(Collection $budgets, Collection $accounts, Carbon $start, Carbon $end): array; /** + * @deprecated * @param Carbon $start * @param Carbon $end * @param Collection|null $accounts @@ -101,4 +102,5 @@ interface OperationsRepositoryInterface public function sumExpenses(Carbon $start, Carbon $end, ?Collection $accounts = null, ?Collection $budgets = null, ?TransactionCurrency $currency = null ): array; + } diff --git a/frontend/src/api/v2/budgets/sum.js b/frontend/src/api/v2/budgets/sum.js index 4b24741a51..926004bf5c 100644 --- a/frontend/src/api/v2/budgets/sum.js +++ b/frontend/src/api/v2/budgets/sum.js @@ -29,10 +29,10 @@ export default class Sum { return api.get(url, {params: {start: startStr, end: endStr}}); } - // /*paid(start, end) { - // let url = 'api/v2/bills/sum/paid'; - // let startStr = format(start, 'y-MM-dd'); - // let endStr = format(end, 'y-MM-dd'); - // return api.get(url, {params: {start: startStr, end: endStr}}); - // }*/ + spent(start, end) { + let url = 'api/v2/budgets/sum/spent'; + let startStr = format(start, 'y-MM-dd'); + let endStr = format(end, 'y-MM-dd'); + return api.get(url, {params: {start: startStr, end: endStr}}); + } } diff --git a/frontend/src/components/dashboard/SpendInsightBox.vue b/frontend/src/components/dashboard/SpendInsightBox.vue index c2d0340435..76b80a5127 100644 --- a/frontend/src/components/dashboard/SpendInsightBox.vue +++ b/frontend/src/components/dashboard/SpendInsightBox.vue @@ -26,7 +26,7 @@ - Spend + To spend and left @@ -48,6 +48,10 @@ Budgeted: ({{ formatAmount(budget.code, budget.sum) }}), +
+ Spent: + ({{ formatAmount(budget.code, budget.sum) }}),
@@ -118,7 +122,7 @@ export default { const sum = new Sum; this.currency = this.store.getCurrencyCode; sum.budgeted(start, end).then((response) => this.parseBudgetedResponse(response.data)); - //sum.paid(start, end).then((response) => this.parsePaidResponse(response.data)); + sum.spent(start, end).then((response) => this.parseSpentResponse(response.data)); } }, // TODO this method is recycled a lot. @@ -143,6 +147,24 @@ export default { } } }, + parseSpentResponse: function (data) { + for (let i in data) { + if (data.hasOwnProperty(i)) { + const current = data[i]; + const hasNative = current.native_id !== current.id && parseFloat(current.native_sum) !== 0.0; + this.spent.push( + { + sum: current.sum, + code: current.code, + native: hasNative + } + ); + if (hasNative || current.native_id === current.id) { + this.spentAmount = this.spentAmount + (parseFloat(current.native_sum) * -1); + } + } + } + }, } } diff --git a/routes/api.php b/routes/api.php index fcca9b92b4..31773aa7cc 100644 --- a/routes/api.php +++ b/routes/api.php @@ -46,14 +46,14 @@ Route::group( ); /** - * V2 API route for bills. + * V2 API route for budgets. */ Route::group( ['namespace' => 'FireflyIII\Api\V2\Controllers\Model\Budget', 'prefix' => 'v2/budgets', 'as' => 'api.v2.budgets',], static function () { Route::get('sum/budgeted', ['uses' => 'SumController@budgeted', 'as' => 'sum.budgeted']); - Route::get('sum/unpaid', ['uses' => 'SumController@unpaid', 'as' => 'sum.unpaid']); + Route::get('sum/spent', ['uses' => 'SumController@spent', 'as' => 'sum.spent']); } );