Add code for budgets

This commit is contained in:
James Cole 2022-06-06 16:41:54 +02:00
parent d007db166a
commit c916fbbee9
No known key found for this signature in database
GPG Key ID: B49A324B7EAD6D80
10 changed files with 280 additions and 64 deletions

View File

@ -0,0 +1,56 @@
<?php
/*
* Controller.php
* Copyright (c) 2022 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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();
}
}

View File

@ -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);
}
}

View File

@ -0,0 +1,46 @@
<?php
/*
* PreferencesController.php
* Copyright (c) 2022 james@firefly-iii.org
*
* This file is part of Firefly III (https://github.com/firefly-iii).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@ -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;
}
}

View File

@ -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
*

View File

@ -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

View File

@ -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;
}

View File

@ -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}});
}
}

View File

@ -26,7 +26,7 @@
<q-card bordered>
<q-item>
<q-item-section>
<q-item-label><strong>Spend</strong></q-item-label>
<q-item-label><strong>To spend and left</strong></q-item-label>
</q-item-section>
</q-item>
<q-separator/>
@ -48,6 +48,10 @@
<span :title="formatAmount(this.currency, this.budgetedAmount)">Budgeted</span>:
<span v-for="(budget, index) in budgeted"><span v-if="budget.native">(</span>{{ formatAmount(budget.code, budget.sum) }}<span v-if="budget.native">)</span><span
v-if="index+1 !== budgeted.length">, </span></span>
<br>
<span :title="formatAmount(this.currency, this.spentAmount)">Spent</span>:
<span v-for="(budget, index) in spent"><span v-if="budget.native">(</span>{{ formatAmount(budget.code, budget.sum) }}<span v-if="budget.native">)</span><span
v-if="index+1 !== budgeted.length">, </span></span>
</q-card-section>
</q-card-section>
</q-card>
@ -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);
}
}
}
},
}
}
</script>

View File

@ -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']);
}
);