diff --git a/app/Http/Controllers/BudgetController.php b/app/Http/Controllers/BudgetController.php index a1ef39d63c..70def0eec3 100644 --- a/app/Http/Controllers/BudgetController.php +++ b/app/Http/Controllers/BudgetController.php @@ -15,10 +15,10 @@ namespace FireflyIII\Http\Controllers; use Amount; use Carbon\Carbon; -use Config; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Collector\JournalCollector; use FireflyIII\Http\Requests\BudgetFormRequest; +use FireflyIII\Http\Requests\BudgetIncomeRequest; use FireflyIII\Models\AccountType; use FireflyIII\Models\Budget; use FireflyIII\Models\LimitRepetition; @@ -26,8 +26,6 @@ use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use Illuminate\Support\Collection; use Input; -use Log; -use Navigation; use Preferences; use Response; use Session; @@ -42,6 +40,9 @@ use View; class BudgetController extends Controller { + /** @var BudgetRepositoryInterface */ + private $repository; + /** * */ @@ -55,6 +56,7 @@ class BudgetController extends Controller function ($request, $next) { View::share('title', trans('firefly.budgets')); View::share('mainTitleIcon', 'fa-tasks'); + $this->repository = app(BudgetRepositoryInterface::class); return $next($request); } @@ -73,14 +75,8 @@ class BudgetController extends Controller /** @var Carbon $start */ $start = session('start', Carbon::now()->startOfMonth()); /** @var Carbon $end */ - $end = session('end', Carbon::now()->endOfMonth()); - $viewRange = Preferences::get('viewRange', '1M')->data; - - // is custom view range? - if (session('is_custom_range') === true) { - $viewRange = 'custom'; - } - + $end = session('end', Carbon::now()->endOfMonth()); + $viewRange = Preferences::get('viewRange', '1M')->data; $limitRepetition = $repository->updateLimitAmount($budget, $start, $end, $viewRange, $amount); if ($amount == 0) { $limitRepetition = null; @@ -173,82 +169,27 @@ class BudgetController extends Controller } /** - * @param BudgetRepositoryInterface $repository - * @param AccountRepositoryInterface $accountRepository - * * @return View - * */ - public function index(BudgetRepositoryInterface $repository, AccountRepositoryInterface $accountRepository) + public function index() { - $repository->cleanupBudgets(); + $this->repository->cleanupBudgets(); - $budgets = $repository->getActiveBudgets(); - $inactive = $repository->getInactiveBudgets(); - $spent = '0'; - $budgeted = '0'; - $range = Preferences::get('viewRange', '1M')->data; - $repeatFreq = Config::get('firefly.range_to_repeat_freq.' . $range); - - if (session('is_custom_range') === true) { - $repeatFreq = 'custom'; - } - - /** @var Carbon $start */ - $start = session('start', new Carbon); - /** @var Carbon $end */ + $budgets = $this->repository->getActiveBudgets(); + $inactive = $this->repository->getInactiveBudgets(); + $start = session('start', new Carbon); $end = session('end', new Carbon); - $key = 'budgetIncomeTotal' . $start->format('Ymd') . $end->format('Ymd'); - $budgetIncomeTotal = Preferences::get($key, 1000)->data; - $period = Navigation::periodShow($start, $range); $periodStart = $start->formatLocalized($this->monthAndDayFormat); $periodEnd = $end->formatLocalized($this->monthAndDayFormat); - $accounts = $accountRepository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET, AccountType::CASH]); - $startAsString = $start->format('Y-m-d'); - $endAsString = $end->format('Y-m-d'); - Log::debug('Now at /budgets'); - - // loop the budgets: - /** @var Budget $budget */ - foreach ($budgets as $budget) { - Log::debug(sprintf('Now at budget #%d ("%s")', $budget->id, $budget->name)); - $budget->spent = $repository->spentInPeriod(new Collection([$budget]), $accounts, $start, $end); - $allRepetitions = $repository->getAllBudgetLimitRepetitions($start, $end); - $otherRepetitions = new Collection; - - /** @var LimitRepetition $repetition */ - foreach ($allRepetitions as $repetition) { - if ($repetition->budget_id == $budget->id) { - if ($repetition->budgetLimit->repeat_freq == $repeatFreq - && $repetition->startdate->format('Y-m-d') == $startAsString - && $repetition->enddate->format('Y-m-d') == $endAsString - ) { - // do something - $budget->currentRep = $repetition; - continue; - } - $otherRepetitions->push($repetition); - } - } - $budget->otherRepetitions = $otherRepetitions; - - if (!is_null($budget->currentRep) && !is_null($budget->currentRep->id)) { - $budgeted = bcadd($budgeted, $budget->currentRep->amount); - } - $spent = bcadd($spent, $budget->spent); - - } - - - $defaultCurrency = Amount::getDefaultCurrency(); + $budgetInformation = $this->collectBudgetInformation($budgets, $start, $end); + $defaultCurrency = Amount::getDefaultCurrency(); + $available = $this->repository->getAvailableBudget($defaultCurrency, $start, $end); + $spent = array_sum(array_column($budgetInformation, 'spent')); + $budgeted = array_sum(array_column($budgetInformation, 'budgeted')); return view( - 'budgets.index', compact( - 'periodStart', 'periodEnd', - 'period', 'range', 'budgetIncomeTotal', - 'defaultCurrency', 'inactive', 'budgets', - 'spent', 'budgeted' - ) + 'budgets.index', + compact('available', 'periodStart', 'periodEnd', 'budgetInformation', 'defaultCurrency', 'inactive', 'budgets', 'spent', 'budgeted') ); } @@ -280,16 +221,14 @@ class BudgetController extends Controller /** * @return \Illuminate\Http\RedirectResponse */ - public function postUpdateIncome() + public function postUpdateIncome(BudgetIncomeRequest $request) { - $range = Preferences::get('viewRange', '1M')->data; - /** @var Carbon $date */ - $date = session('start', new Carbon); - $start = Navigation::startOfPeriod($date, $range); - $end = Navigation::endOfPeriod($start, $range); - $key = 'budgetIncomeTotal' . $start->format('Ymd') . $end->format('Ymd'); + $start = session('start', new Carbon); + $end = session('end', new Carbon); + $defaultCurrency = Amount::getDefaultCurrency(); + $amount = $request->get('amount'); - Preferences::set($key, intval(Input::get('amount'))); + $this->repository->setAvailableBudget($defaultCurrency, $start, $end, $amount); Preferences::mark(); return redirect(route('budgets.index')); @@ -430,19 +369,57 @@ class BudgetController extends Controller */ public function updateIncome() { - $range = Preferences::get('viewRange', '1M')->data; - $format = strval(trans('config.month_and_day')); + $start = session('start', new Carbon); + $end = session('end', new Carbon); + $defaultCurrency = Amount::getDefaultCurrency(); + $available = $this->repository->getAvailableBudget($defaultCurrency, $start, $end); - /** @var Carbon $date */ - $date = session('start', new Carbon); - $start = Navigation::startOfPeriod($date, $range); - $end = Navigation::endOfPeriod($start, $range); - $key = 'budgetIncomeTotal' . $start->format('Ymd') . $end->format('Ymd'); - $amount = Preferences::get($key, 1000); - $displayStart = $start->formatLocalized($format); - $displayEnd = $end->formatLocalized($format); - return view('budgets.income', compact('amount', 'displayStart', 'displayEnd')); + return view('budgets.income', compact('available', 'start', 'end')); + } + + /** + * @param Collection $budgets + * @param Carbon $start + * @param Carbon $end + * + * @return array + */ + private function collectBudgetInformation(Collection $budgets, Carbon $start, Carbon $end): array + { + // get account information + $accountRepository = app(AccountRepositoryInterface::class); + $accounts = $accountRepository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET, AccountType::CASH]); + $return = []; + /** @var Budget $budget */ + foreach ($budgets as $budget) { + $budgetId = $budget->id; + $return[$budgetId] = [ + 'spent' => $this->repository->spentInPeriod(new Collection([$budget]), $accounts, $start, $end), + 'budgeted' => '0', + 'currentRep' => false, + ]; + $allRepetitions = $this->repository->getAllBudgetLimitRepetitions($start, $end); + $otherRepetitions = new Collection; + + // get all the limit repetitions relevant between start and end and examine them: + /** @var LimitRepetition $repetition */ + foreach ($allRepetitions as $repetition) { + if ($repetition->budget_id == $budget->id) { + if ($repetition->startdate->isSameDay($start) && $repetition->enddate->isSameDay($end) + ) { + $return[$budgetId]['currentRep'] = $repetition; + $return[$budgetId]['budgeted'] = $repetition->amount; + continue; + } + // otherwise it's just one of the many relevant repetitions: + $otherRepetitions->push($repetition); + } + } + $return[$budgetId]['otherRepetitions'] = $otherRepetitions; + } + + return $return; } } diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index 6882a0f14a..9a4b2bf7f5 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -63,6 +63,7 @@ class HomeController extends Controller // a possible problem with the budgets. if ($label === strval(trans('firefly.everything')) || $label === strval(trans('firefly.customRange'))) { $isCustomRange = true; + Preferences::set('viewRange', 'custom'); Log::debug('Range is now marked as "custom".'); } diff --git a/app/Http/Requests/BudgetIncomeRequest.php b/app/Http/Requests/BudgetIncomeRequest.php new file mode 100644 index 0000000000..e8c2c93e39 --- /dev/null +++ b/app/Http/Requests/BudgetIncomeRequest.php @@ -0,0 +1,42 @@ +check(); + } + + /** + * @return array + */ + public function rules() + { + return [ + 'amount' => 'numeric|required|min:0', + ]; + } +} diff --git a/app/Models/AvailableBudget.php b/app/Models/AvailableBudget.php new file mode 100644 index 0000000000..6916052c81 --- /dev/null +++ b/app/Models/AvailableBudget.php @@ -0,0 +1,49 @@ +belongsTo('FireflyIII\Models\TransactionCurrency'); + } + + /** + * @return BelongsTo + */ + public function user(): BelongsTo + { + return $this->belongsTo('FireflyIII\User'); + } +} \ No newline at end of file diff --git a/app/Repositories/Budget/BudgetRepository.php b/app/Repositories/Budget/BudgetRepository.php index f4633d5ec3..b672444fbd 100644 --- a/app/Repositories/Budget/BudgetRepository.php +++ b/app/Repositories/Budget/BudgetRepository.php @@ -17,10 +17,12 @@ use Carbon\Carbon; use FireflyIII\Events\StoredBudgetLimit; use FireflyIII\Events\UpdatedBudgetLimit; use FireflyIII\Helpers\Collector\JournalCollectorInterface; +use FireflyIII\Models\AvailableBudget; use FireflyIII\Models\Budget; use FireflyIII\Models\BudgetLimit; use FireflyIII\Models\LimitRepetition; use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; use FireflyIII\User; @@ -210,6 +212,27 @@ class BudgetRepository implements BudgetRepositoryInterface return $set; } + /** + * @param TransactionCurrency $currency + * @param Carbon $start + * @param Carbon $end + * + * @return string + */ + public function getAvailableBudget(TransactionCurrency $currency, Carbon $start, Carbon $end): string + { + $amount = '0'; + $availableBudget = $this->user->availableBudgets() + ->where('transaction_currency_id', $currency->id) + ->where('start_date', $start->format('Y-m-d')) + ->where('end_date', $end->format('Y-m-d'))->first(); + if (!is_null($availableBudget)) { + $amount = strval($availableBudget->amount); + } + + return $amount; + } + /** * This method is being used to generate the budget overview in the year/multi-year report. Its used * in both the year/multi-year budget overview AND in the accompanying chart. @@ -322,6 +345,33 @@ class BudgetRepository implements BudgetRepositoryInterface return $result; } + /** + * @param TransactionCurrency $currency + * @param Carbon $start + * @param Carbon $end + * @param string $amount + * + * @return bool + */ + public function setAvailableBudget(TransactionCurrency $currency, Carbon $start, Carbon $end, string $amount): bool + { + $availableBudget = $this->user->availableBudgets() + ->where('transaction_currency_id', $currency->id) + ->where('start_date', $start->format('Y-m-d')) + ->where('end_date', $end->format('Y-m-d'))->first(); + if (is_null($availableBudget)) { + $availableBudget = new AvailableBudget; + $availableBudget->user()->associate($this->user); + $availableBudget->transactionCurrency()->associate($currency); + $availableBudget->start_date = $start; + $availableBudget->end_date = $end; + } + $availableBudget->amount = $amount; + $availableBudget->save(); + + return true; + } + /** * @param Collection $budgets * @param Collection $accounts diff --git a/app/Repositories/Budget/BudgetRepositoryInterface.php b/app/Repositories/Budget/BudgetRepositoryInterface.php index 30633beff9..8d04c7bff6 100644 --- a/app/Repositories/Budget/BudgetRepositoryInterface.php +++ b/app/Repositories/Budget/BudgetRepositoryInterface.php @@ -16,6 +16,7 @@ namespace FireflyIII\Repositories\Budget; use Carbon\Carbon; use FireflyIII\Models\Budget; use FireflyIII\Models\BudgetLimit; +use FireflyIII\Models\TransactionCurrency; use Illuminate\Support\Collection; /** @@ -90,6 +91,15 @@ interface BudgetRepositoryInterface */ public function getAllBudgetLimitRepetitions(Carbon $start, Carbon $end): Collection; + /** + * @param TransactionCurrency $currency + * @param Carbon $start + * @param Carbon $end + * + * @return string + */ + public function getAvailableBudget(TransactionCurrency $currency, Carbon $start, Carbon $end): string; + /** * * @param Collection $budgets @@ -120,6 +130,16 @@ interface BudgetRepositoryInterface */ public function getNoBudgetPeriodReport(Collection $accounts, Carbon $start, Carbon $end): array; + /** + * @param TransactionCurrency $currency + * @param Carbon $start + * @param Carbon $end + * @param string $amount + * + * @return bool + */ + public function setAvailableBudget(TransactionCurrency $currency, Carbon $start, Carbon $end, string $amount): bool; + /** * @param Collection $budgets * @param Collection $accounts diff --git a/app/User.php b/app/User.php index ba57a8ce47..763d33d92c 100755 --- a/app/User.php +++ b/app/User.php @@ -51,6 +51,15 @@ class User extends Authenticatable */ protected $table = 'users'; + + /** + * @return HasMany + */ + public function availableBudgets(): HasMany + { + return $this->hasMany('FireflyIII\Models\AvailableBudget'); + } + /** * @return HasMany */ diff --git a/database/migrations/2016_12_22_150431_changes_for_v430.php b/database/migrations/2016_12_22_150431_changes_for_v430.php new file mode 100644 index 0000000000..bf98ad68dc --- /dev/null +++ b/database/migrations/2016_12_22_150431_changes_for_v430.php @@ -0,0 +1,44 @@ +increments('id'); + $table->timestamps(); + $table->softDeletes(); + $table->integer('user_id', false, true); + $table->integer('transaction_currency_id', false, true); + $table->decimal('amount', 14, 4); + $table->date('start_date'); + $table->date('end_date'); + + + $table->foreign('transaction_currency_id')->references('id')->on('transaction_currencies')->onDelete('cascade'); + $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('available_budgets'); + } +} diff --git a/public/js/ff/budgets/index.js b/public/js/ff/budgets/index.js index c8ca145734..ce0103ef1a 100644 --- a/public/js/ff/budgets/index.js +++ b/public/js/ff/budgets/index.js @@ -1,5 +1,3 @@ -/* globals $, budgeted:true, currencySymbol, budgetIncomeTotal, columnChart, budgetedMuch, budgetedPercentage, token, budgetID, repetitionID, spent, lineChart */ - function drawSpentBar() { "use strict"; if ($('.spentBar').length > 0) { @@ -23,19 +21,19 @@ function drawBudgetedBar() { "use strict"; if ($('.budgetedBar').length > 0) { - var budgetedMuch = budgeted > budgetIncomeTotal; + var budgetedMuch = budgeted > available; // recalculate percentage: var pct; if (budgetedMuch) { // budgeted too much. - pct = (budgetIncomeTotal / budgeted) * 100; + pct = (available / budgeted) * 100; $('.budgetedBar .progress-bar-warning').css('width', pct + '%'); $('.budgetedBar .progress-bar-danger').css('width', (100 - pct) + '%'); $('.budgetedBar .progress-bar-info').css('width', 0); } else { - pct = (budgeted / budgetIncomeTotal) * 100; + pct = (budgeted / available) * 100; $('.budgetedBar .progress-bar-warning').css('width', 0); $('.budgetedBar .progress-bar-danger').css('width', 0); $('.budgetedBar .progress-bar-info').css('width', pct + '%'); diff --git a/resources/views/budgets/income.twig b/resources/views/budgets/income.twig index f78cb2c255..28904dc66d 100644 --- a/resources/views/budgets/income.twig +++ b/resources/views/budgets/income.twig @@ -4,7 +4,8 @@ @@ -14,7 +15,7 @@
{{ getCurrencySymbol()|raw }}
- +
{{ trans('firefly.available_between',{start : periodStart, end: periodEnd }) }}: - {{ budgetIncomeTotal|formatAmount }} + {{ available|formatAmountPlain }}
@@ -90,8 +90,8 @@

- {% if budget.currentRep.id %} - {{ budget.name }} {% else %} {{ budget.name }} @@ -122,20 +122,16 @@
{{ defaultCurrency.symbol|raw }}
- - + {% if budgetInformation[budget.id]['currentRep'] %} + {% set repAmount = budgetInformation[budget.id]['currentRep'].amount %} + {% else %} + {% set repAmount = '0' %} + {% endif %} +
-
@@ -147,21 +143,23 @@ {{ session('end').formatLocalized(monthAndDayFormat) }} - {{ budget.spent|formatAmount }} + + {{ budgetInformation[budget.id]['spent']|formatAmount }} + - {% if budget.otherRepetitions.count > 0 %} + {% if budgetInformation[budget.id]['otherRepetitions'].count > 0 %} @@ -206,7 +204,7 @@ // budgeted data: var budgeted = {{ budgeted }}; - var budgetIncomeTotal = {{ budgetIncomeTotal }}; + var available = {{ available }};