Fix some routes for the budget report.

This commit is contained in:
James Cole
2016-12-16 08:07:31 +01:00
parent b021c7690f
commit 4de14eba0c
14 changed files with 414 additions and 139 deletions

View File

@@ -58,7 +58,7 @@ class MonthReportGenerator extends Support implements ReportGeneratorInterface
public function generate(): string
{
$accountIds = join(',', $this->accounts->pluck('id')->toArray());
$categoryIds = join(',', $this->budgets->pluck('id')->toArray());
$budgetIds = join(',', $this->budgets->pluck('id')->toArray());
$expenses = $this->getExpenses();
$accountSummary = $this->summarizeByAccount($expenses);
$budgetSummary = $this->summarizeByBudget($expenses);
@@ -66,7 +66,7 @@ class MonthReportGenerator extends Support implements ReportGeneratorInterface
$topExpenses = $this->getTopExpenses();
// render!
return view('reports.budget.month', compact('accountIds', 'categoryIds', 'accountSummary', 'budgetSummary', 'averageExpenses', 'topExpenses'))
return view('reports.budget.month', compact('accountIds', 'budgetIds', 'accountSummary', 'budgetSummary', 'averageExpenses', 'topExpenses'))
->with('start', $this->start)->with('end', $this->end)
->with('budgets', $this->budgets)
->with('accounts', $this->accounts)

View File

@@ -30,16 +30,6 @@ use Lang;
*/
class LoginController extends Controller
{
/*
|--------------------------------------------------------------------------
| Login Controller
|--------------------------------------------------------------------------
|
| This controller handles authenticating users for the application and
| redirecting them to your home screen. The controller uses a trait
| to conveniently provide its functionality to your applications.
|
*/
use AuthenticatesUsers;

View File

@@ -31,16 +31,6 @@ use Illuminate\Support\Facades\Password;
*/
class PasswordController extends Controller
{
/*
|--------------------------------------------------------------------------
| Password Reset Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling password reset requests
| and uses a simple trait to include this behavior. You're free to
| explore this trait and override any methods you wish to tweak.
|
*/
use ResetsPasswords;

View File

@@ -36,16 +36,6 @@ use Validator;
*/
class RegisterController extends Controller
{
/*
|--------------------------------------------------------------------------
| Register Controller
|--------------------------------------------------------------------------
|
| This controller handles the registration of new users as well as their
| validation and creation. By default this controller uses a trait to
| provide this functionality without requiring any additional code.
|
*/
use RegistersUsers;

View File

@@ -22,16 +22,6 @@ use Illuminate\Foundation\Auth\ResetsPasswords;
*/
class ResetPasswordController extends Controller
{
/*
|--------------------------------------------------------------------------
| Password Reset Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling password reset requests
| and uses a simple trait to include this behavior. You're free to
| explore this trait and override any methods you wish to tweak.
|
*/
use ResetsPasswords;

View File

@@ -65,8 +65,11 @@ class AccountController extends Controller
$cache->addProperty('chart.account.all');
$cache->addProperty($account->id);
if ($cache->has()) {
Log::debug('Return chart.account.all from cache.');
return Response::json($cache->get());
}
Log::debug('Regenerate chart.account.all from scratch.');
/** @var AccountRepositoryInterface $repository */
$repository = app(AccountRepositoryInterface::class);
@@ -458,8 +461,11 @@ class AccountController extends Controller
$cache->addProperty('chart.account.account-balance-chart');
$cache->addProperty($accounts);
if ($cache->has()) {
Log::debug('Return chart.account.account-balance-chart from cache.');
return $cache->get();
}
Log::debug('Regenerate chart.account.account-balance-chart from scratch.');
$chartData = [];
foreach ($accounts as $account) {

View File

@@ -0,0 +1,292 @@
<?php
/**
* BudgetReportController.php
* Copyright (C) 2016 thegrumpydictator@gmail.com
*
* This software may be modified and distributed under the terms of the
* Creative Commons Attribution-ShareAlike 4.0 International License.
*
* See the LICENSE file for details.
*/
declare(strict_types = 1);
namespace FireflyIII\Http\Controllers\Chart;
use Carbon\Carbon;
use FireflyIII\Generator\Chart\Basic\GeneratorInterface;
use FireflyIII\Generator\Report\Category\MonthReportGenerator;
use FireflyIII\Helpers\Collector\JournalCollector;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\Budget;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Support\CacheProperties;
use Illuminate\Support\Collection;
use Navigation;
use Response;
/**
* Separate controller because many helper functions are shared.
*
* Class BudgetReportController
*
* @package FireflyIII\Http\Controllers\Chart
*/
class BudgetReportController extends Controller
{
/** @var AccountRepositoryInterface */
private $accountRepository;
/** @var BudgetRepositoryInterface */
private $budgetRepository;
/** @var GeneratorInterface */
private $generator;
/**
*
*/
public function __construct()
{
parent::__construct();
$this->middleware(
function ($request, $next) {
$this->generator = app(GeneratorInterface::class);
$this->budgetRepository = app(BudgetRepositoryInterface::class);
$this->accountRepository = app(AccountRepositoryInterface::class);
return $next($request);
}
);
}
/**
* @param Collection $accounts
* @param Collection $budgets
* @param Carbon $start
* @param Carbon $end
* @param string $others
*
* @return \Illuminate\Http\JsonResponse
*/
public function accountExpense(Collection $accounts, Collection $budgets, Carbon $start, Carbon $end, string $others)
{
/** @var bool $others */
$others = intval($others) === 1;
$cache = new CacheProperties;
$cache->addProperty('chart.budget.report.account-expense');
$cache->addProperty($accounts);
$cache->addProperty($budgets);
$cache->addProperty($start);
$cache->addProperty($end);
if ($cache->has()) {
return Response::json($cache->get());
}
$names = [];
$set = $this->getExpenses($accounts, $budgets, $start, $end);
$grouped = $this->groupByOpposingAccount($set);
$chartData = [];
$total = '0';
foreach ($grouped as $accountId => $amount) {
if (!isset($names[$accountId])) {
$account = $this->accountRepository->find(intval($accountId));
$names[$accountId] = $account->name;
}
$amount = bcmul($amount, '-1');
$total = bcadd($total, $amount);
$chartData[$names[$accountId]] = $amount;
}
// also collect all transactions NOT in these budgets.
if ($others) {
$collector = new JournalCollector(auth()->user());
$collector->setAccounts($accounts)->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL]);
$journals = $collector->getJournals();
$sum = strval($journals->sum('transaction_amount'));
$sum = bcmul($sum, '-1');
$sum = bcsub($sum, $total);
$chartData[strval(trans('firefly.everything_else'))] = $sum;
}
$data = $this->generator->pieChart($chartData);
$cache->store($data);
return Response::json($data);
}
/**
* @param Collection $accounts
* @param Collection $budgets
* @param Carbon $start
* @param Carbon $end
* @param string $others
*
* @return \Illuminate\Http\JsonResponse
*/
public function budgetExpense(Collection $accounts, Collection $budgets, Carbon $start, Carbon $end, string $others)
{
/** @var bool $others */
$others = intval($others) === 1;
$cache = new CacheProperties;
$cache->addProperty('chart.budget.report.budget-expense');
$cache->addProperty($accounts);
$cache->addProperty($budgets);
$cache->addProperty($start);
$cache->addProperty($end);
if ($cache->has()) {
return Response::json($cache->get());
}
$names = [];
$set = $this->getExpenses($accounts, $budgets, $start, $end);
$grouped = $this->groupByBudget($set);
$total = '0';
$chartData = [];
foreach ($grouped as $budgetId => $amount) {
if (!isset($names[$budgetId])) {
$budget = $this->budgetRepository->find(intval($budgetId));
$names[$budgetId] = $budget->name;
}
$amount = bcmul($amount, '-1');
$total = bcadd($total, $amount);
$chartData[$names[$budgetId]] = $amount;
}
// also collect all transactions NOT in these budgets.
if ($others) {
$collector = new JournalCollector(auth()->user());
$collector->setAccounts($accounts)->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL]);
$journals = $collector->getJournals();
$sum = strval($journals->sum('transaction_amount'));
$sum = bcmul($sum, '-1');
$sum = bcsub($sum, $total);
$chartData[strval(trans('firefly.everything_else'))] = $sum;
}
$data = $this->generator->pieChart($chartData);
$cache->store($data);
return Response::json($data);
}
/**
* @param Collection $accounts
* @param Collection $budgets
* @param Carbon $start
* @param Carbon $end
*
* @return \Illuminate\Http\JsonResponse
*/
public function mainChart(Collection $accounts, Collection $budgets, Carbon $start, Carbon $end)
{
$cache = new CacheProperties;
$cache->addProperty('chart.budget.report.main');
$cache->addProperty($accounts);
$cache->addProperty($budgets);
$cache->addProperty($start);
$cache->addProperty($end);
if ($cache->has()) {
return Response::json($cache->get());
}
$format = Navigation::preferredCarbonLocalizedFormat($start, $end);
$function = Navigation::preferredEndOfPeriod($start, $end);
$chartData = [];
$currentStart = clone $start;
// prep chart data:
foreach ($budgets as $budget) {
$chartData[$budget->id] = [
'label' => $budget->name,
'type' => 'bar',
'entries' => [],
];
}
while ($currentStart < $end) {
$currentEnd = clone $currentStart;
$currentEnd = $currentEnd->$function();
$expenses = $this->groupByBudget($this->getExpenses($accounts, $budgets, $currentStart, $currentEnd));
$label = $currentStart->formatLocalized($format);
/** @var Budget $budget */
foreach ($budgets as $budget) {
$chartData[$budget->id]['entries'][$label] = $expenses[$budget->id] ?? '0';
}
$currentStart = clone $currentEnd;
$currentStart->addDay();
}
$data = $this->generator->multiSet($chartData);
$cache->store($data);
return Response::json($data);
}
/**
* @param Collection $accounts
* @param Collection $budgets
* @param Carbon $start
* @param Carbon $end
*
* @return Collection
*/
private function getExpenses(Collection $accounts, Collection $budgets, Carbon $start, Carbon $end): Collection
{
$collector = new JournalCollector(auth()->user());
$collector->setAccounts($accounts)->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL, TransactionType::TRANSFER])
->setBudgets($budgets)->withOpposingAccount()->disableFilter();
$accountIds = $accounts->pluck('id')->toArray();
$transactions = $collector->getJournals();
$set = MonthReportGenerator::filterExpenses($transactions, $accountIds);
return $set;
}
/**
* @param Collection $set
*
* @return array
*/
private function groupByBudget(Collection $set): array
{
// group by category ID:
$grouped = [];
/** @var Transaction $transaction */
foreach ($set as $transaction) {
$jrnlBudId = intval($transaction->transaction_journal_budget_id);
$transBudId = intval($transaction->transaction_budget_id);
$budgetId = max($jrnlBudId, $transBudId);
$grouped[$budgetId] = $grouped[$budgetId] ?? '0';
$grouped[$budgetId] = bcadd($transaction->transaction_amount, $grouped[$budgetId]);
}
return $grouped;
}
/**
* @param Collection $set
*
* @return array
*/
private function groupByOpposingAccount(Collection $set): array
{
$grouped = [];
/** @var Transaction $transaction */
foreach ($set as $transaction) {
$accountId = $transaction->opposing_account_id;
$grouped[$accountId] = $grouped[$accountId] ?? '0';
$grouped[$accountId] = bcadd($transaction->transaction_amount, $grouped[$accountId]);
}
return $grouped;
}
}

View File

@@ -22,16 +22,6 @@ use Illuminate\Bus\Queueable;
*/
abstract class Job
{
/*
|--------------------------------------------------------------------------
| Queueable Jobs
|--------------------------------------------------------------------------
|
| This job base class provides a central location to place any logic that
| is shared across all of your jobs. The trait included with the class
| provides access to the "onQueue" and "delay" queue helper methods.
|
*/
use Queueable;
}

View File

@@ -211,7 +211,6 @@ class BillRepository implements BillRepositoryInterface
$sum = bcadd($sum, $amount);
Log::debug(sprintf('Total > 0, so add to sum %f, which becomes %f', $amount, $sum));
}
Log::debug('---');
}
return $sum;
@@ -245,7 +244,6 @@ class BillRepository implements BillRepositoryInterface
$sum = bcadd($sum, $multi);
Log::debug(sprintf('Total > 0, so add to sum %f, which becomes %f', $multi, $sum));
}
Log::debug('---');
}
return $sum;

View File

@@ -18,6 +18,7 @@ use Cache;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Collection;
use Log;
use Preferences as Prefs;
/**
@@ -75,6 +76,8 @@ class CacheProperties
public function has(): bool
{
if (getenv('APP_ENV') == 'testing') {
Log::debug('APP_ENV is testing, cache disabled.');
return false;
}
$this->md5();
@@ -118,7 +121,8 @@ class CacheProperties
$this->md5 .= json_encode($property);
}
Log::debug(sprintf('Cache string is %s', $this->md5));
$this->md5 = md5($this->md5);
Log::debug(sprintf('Cache MD5 is %s', $this->md5));
}
}

View File

@@ -13,16 +13,8 @@ $(function () {
"use strict";
drawChart();
$('#budgets-in-pie-chart-checked').on('change', function () {
redrawPieChart('budgets-in-pie-chart', categoryIncomeUri);
});
$('#budgets-out-pie-chart-checked').on('change', function () {
redrawPieChart('budgets-out-pie-chart', categoryExpenseUri);
});
$('#accounts-in-pie-chart-checked').on('change', function () {
redrawPieChart('accounts-in-pie-chart', accountIncomeUri);
redrawPieChart('budgets-out-pie-chart', budgetExpenseUri);
});
$('#accounts-out-pie-chart-checked').on('change', function () {
@@ -39,9 +31,7 @@ function drawChart() {
stackedColumnChart(mainUri, 'in-out-chart');
// draw pie chart of income, depending on "show other transactions too":
redrawPieChart('budgets-in-pie-chart', categoryIncomeUri);
redrawPieChart('budgets-out-pie-chart', categoryExpenseUri);
redrawPieChart('accounts-in-pie-chart', accountIncomeUri);
redrawPieChart('budgets-out-pie-chart', budgetExpenseUri);
redrawPieChart('accounts-out-pie-chart', accountExpenseUri);

View File

@@ -84,22 +84,20 @@
</div>
</div>
{% endif %}
{% if accounts.count > 1 %}
<div class="col-lg-4 col-md-6">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">{{ 'expense_per_budget'|_ }}</h3>
</div>
<div class="box-body">
<canvas id="accounts-out-pie-chart" style="margin:0 auto;" height="150" width="150"></canvas>
<label style="font-weight:normal;">
<input type="checkbox" id="accounts-out-pie-chart-checked">
<small>{{ 'include_not_in_budget'|_ }}</small>
</label>
</div>
<div class="col-lg-4 col-md-6">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">{{ 'expense_per_account'|_ }}</h3>
</div>
<div class="box-body">
<canvas id="accounts-out-pie-chart" style="margin:0 auto;" height="150" width="150"></canvas>
<label style="font-weight:normal;">
<input type="checkbox" id="accounts-out-pie-chart-checked">
<small>{{ 'include_not_in_budget'|_ }}</small>
</label>
</div>
</div>
{% endif %}
</div>
</div>
<div class="row">
@@ -137,22 +135,35 @@
</thead>
<tbody>
{% for row in averageExpenses %}
<tr>
<td data-value="{{ row.name }}">
<a href="{{ route('accounts.show', row.id) }}">{{ row.name }}</a>
</td>
<td data-value="{{ row.average }}">
{{ row.average|formatAmount }}
</td>
<td data-value="{{ row.sum }}">
{{ row.sum|formatAmount }}
</td>
<td data-value="{{ row.count }}">
{{ row.count }}
</td>
{% if loop.index > listLength %}
<tr class="overListLength">
{% else %}
<tr>
{% endif %}
<td data-value="{{ row.name }}">
<a href="{{ route('accounts.show', row.id) }}">{{ row.name }}</a>
</td>
<td data-value="{{ row.average }}">
{{ row.average|formatAmount }}
</td>
<td data-value="{{ row.sum }}">
{{ row.sum|formatAmount }}
</td>
<td data-value="{{ row.count }}">
{{ row.count }}
</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
{% if averageExpenses|length > listLength %}
<tr>
<td colspan="4" class="active">
<a href="#" class="listLengthTrigger">{{ trans('firefly.show_full_list',{number:incomeTopLength}) }}</a>
</td>
</tr>
{% endif %}
</tfoot>
</table>
</div>
</div>
@@ -206,9 +217,9 @@
{% endfor %}
</tbody>
<tfoot>
{% if topExpenses.count > listLength %}
{% if topExpenses|length > listLength %}
<tr>
<td colspan="3" class="active">
<td colspan="4" class="active">
<a href="#" class="listLengthTrigger">{{ trans('firefly.show_full_list',{number:incomeTopLength}) }}</a>
</td>
</tr>
@@ -237,9 +248,7 @@
var budgetIds = '{{ budgetIds }}';
// chart uri's
var budgetIncomeUri = '{{ route('chart.budget.budget-income', [accountIds, budgetIds, start.format('Ymd'), end.format('Ymd'),'OTHERS']) }}';
var budgetExpenseUri = '{{ route('chart.budget.budget-expense', [accountIds, budgetIds, start.format('Ymd'), end.format('Ymd'),'OTHERS']) }}';
var accountIncomeUri = '{{ route('chart.budget.account-income', [accountIds, budgetIds, start.format('Ymd'), end.format('Ymd'),'OTHERS']) }}';
var accountExpenseUri = '{{ route('chart.budget.account-expense', [accountIds, budgetIds, start.format('Ymd'), end.format('Ymd'),'OTHERS']) }}';
var mainUri = '{{ route('chart.budget.main', [accountIds, budgetIds, start.format('Ymd'), end.format('Ymd')]) }}';
</script>

View File

@@ -110,36 +110,34 @@
</div>
</div>
{% endif %}
{% if accounts.count > 1 %}
<div class="col-lg-2 col-md-3">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">{{ 'income_per_account'|_ }}</h3>
</div>
<div class="box-body">
<canvas id="accounts-in-pie-chart" style="margin:0 auto;" height="150" width="150"></canvas>
<label style="font-weight:normal;">
<input type="checkbox" id="accounts-in-pie-chart-checked">
<small>{{ 'include_not_in_category'|_ }}</small>
</label>
</div>
<div class="col-lg-2 col-md-3">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">{{ 'income_per_account'|_ }}</h3>
</div>
<div class="box-body">
<canvas id="accounts-in-pie-chart" style="margin:0 auto;" height="150" width="150"></canvas>
<label style="font-weight:normal;">
<input type="checkbox" id="accounts-in-pie-chart-checked">
<small>{{ 'include_not_in_category'|_ }}</small>
</label>
</div>
</div>
<div class="col-lg-2 col-md-3">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">{{ 'expense_per_account'|_ }}</h3>
</div>
<div class="box-body">
<canvas id="accounts-out-pie-chart" style="margin:0 auto;" height="150" width="150"></canvas>
<label style="font-weight:normal;">
<input type="checkbox" id="accounts-out-pie-chart-checked">
<small>{{ 'include_not_in_category'|_ }}</small>
</label>
</div>
</div>
<div class="col-lg-2 col-md-3">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">{{ 'expense_per_account'|_ }}</h3>
</div>
<div class="box-body">
<canvas id="accounts-out-pie-chart" style="margin:0 auto;" height="150" width="150"></canvas>
<label style="font-weight:normal;">
<input type="checkbox" id="accounts-out-pie-chart-checked">
<small>{{ 'include_not_in_category'|_ }}</small>
</label>
</div>
</div>
{% endif %}
</div>
</div>
<div class="row">
@@ -173,22 +171,35 @@
</thead>
<tbody>
{% for row in averageExpenses %}
<tr>
<td data-value="{{ row.name }}">
<a href="{{ route('accounts.show', row.id) }}">{{ row.name }}</a>
</td>
<td data-value="{{ row.average }}">
{{ row.average|formatAmount }}
</td>
<td data-value="{{ row.sum }}">
{{ row.sum|formatAmount }}
</td>
<td data-value="{{ row.count }}">
{{ row.count }}
</td>
{% if loop.index > listLength %}
<tr class="overListLength">
{% else %}
<tr>
{% endif %}
<td data-value="{{ row.name }}">
<a href="{{ route('accounts.show', row.id) }}">{{ row.name }}</a>
</td>
<td data-value="{{ row.average }}">
{{ row.average|formatAmount }}
</td>
<td data-value="{{ row.sum }}">
{{ row.sum|formatAmount }}
</td>
<td data-value="{{ row.count }}">
{{ row.count }}
</td>
</tr>
{% endfor %}
</tbody>
<tfoot>
{% if averageExpenses|length > listLength %}
<tr>
<td colspan="4" class="active">
<a href="#" class="listLengthTrigger">{{ trans('firefly.show_full_list',{number:incomeTopLength}) }}</a>
</td>
</tr>
{% endif %}
</tfoot>
</table>
</div>
</div>
@@ -242,7 +253,7 @@
{% endfor %}
</tbody>
<tfoot>
{% if topExpenses.count > listLength %}
{% if topExpenses|length > listLength %}
<tr>
<td colspan="3" class="active">
<a href="#" class="listLengthTrigger">{{ trans('firefly.show_full_list',{number:incomeTopLength}) }}</a>

View File

@@ -263,6 +263,21 @@ Route::group(
Route::get('budget/{budget}/{limitrepetition}', ['uses' => 'BudgetController@budgetLimit', 'as' => 'budget-limit']);
Route::get('budget/{budget}', ['uses' => 'BudgetController@budget', 'as' => 'budget']);
// these charts are used in reports (category reports):
Route::get(
'budget/expense/{accountList}/{budgetList}/{start_date}/{end_date}/{others}',
['uses' => 'BudgetReportController@budgetExpense', 'as' => 'budget-expense']
);
Route::get(
'account/expense/{accountList}/{budgetList}/{start_date}/{end_date}/{others}',
['uses' => 'BudgetReportController@accountExpense', 'as' => 'account-expense']
);
Route::get(
'operations/{accountList}/{budgetList}/{start_date}/{end_date}',
['uses' => 'BudgetReportController@mainChart', 'as' => 'main']
);
}
);
@@ -298,7 +313,7 @@ Route::group(
);
Route::get(
'report-in-out/{accountList}/{categoryList}/{start_date}/{end_date}',
'operations/{accountList}/{categoryList}/{start_date}/{end_date}',
['uses' => 'CategoryReportController@mainChart', 'as' => 'main']
);