Building report from issue #386

This commit is contained in:
James Cole 2016-12-03 20:38:13 +01:00
parent 57e49c225b
commit 8377a2a0de
10 changed files with 389 additions and 86 deletions

View File

@ -60,6 +60,8 @@ class JournalCollector implements JournalCollectorInterface
'account_types.type as account_type',
];
/** @var bool */
private $filterInternalTransfers;
/** @var bool */
private $filterTransfers = false;
/** @var bool */
private $joinedBudget = false;
@ -127,6 +129,26 @@ class JournalCollector implements JournalCollectorInterface
return $this;
}
/**
* @return JournalCollectorInterface
*/
public function disableInternalFilter(): JournalCollectorInterface
{
$this->filterInternalTransfers = false;
return $this;
}
/**
* @return JournalCollectorInterface
*/
public function enableInternalFilter(): JournalCollectorInterface
{
$this->filterInternalTransfers = true;
return $this;
}
/**
* @return Collection
*/
@ -138,6 +160,11 @@ class JournalCollector implements JournalCollectorInterface
$set = $this->filterTransfers($set);
Log::debug(sprintf('Count of set after filterTransfers() is %d', $set->count()));
// possibly filter "internal" transfers:
$set = $this->filterInternalTransfers($set);
Log::debug(sprintf('Count of set after filterInternalTransfers() is %d', $set->count()));
// loop for decryption.
$set->each(
function (Transaction $transaction) {
@ -501,6 +528,47 @@ class JournalCollector implements JournalCollectorInterface
return $this;
}
/**
* @param Collection $set
*
* @return Collection
*/
private function filterInternalTransfers(Collection $set): Collection
{
if ($this->filterInternalTransfers === false) {
Log::debug('Did NO filtering for internal transfers on given set.');
return $set;
}
if ($this->joinedOpposing === false) {
Log::error('Cannot filter internal transfers because no opposing information is present.');
return $set;
}
$accountIds = $this->accountIds;
$set = $set->filter(
function (Transaction $transaction) use ($accountIds) {
// both id's in $accountids?
if (in_array($transaction->account_id, $accountIds) && in_array($transaction->opposing_account_id, $accountIds)) {
Log::debug(
sprintf(
'Transaction #%d has #%d and #%d in set, so removed',
$transaction->id, $transaction->account_id, $transaction->opposing_account_id
), $accountIds
);
return false;
}
return $transaction;
}
);
return $set;
}
/**
* If the set of accounts used by the collector includes more than one asset
* account, chances are the set include double entries: transfers get selected
@ -583,6 +651,7 @@ class JournalCollector implements JournalCollectorInterface
private function joinOpposingTables()
{
if (!$this->joinedOpposing) {
Log::debug('joinedOpposing is false');
// join opposing transaction (hard):
$this->query->leftJoin(
'transactions as opposing', function (JoinClause $join) {
@ -595,11 +664,12 @@ class JournalCollector implements JournalCollectorInterface
$this->query->leftJoin('account_types as opposing_account_types', 'opposing_accounts.account_type_id', '=', 'opposing_account_types.id');
$this->query->whereNull('opposing.deleted_at');
$this->fields[] = 'opposing.account_id as opposing_account_id';
$this->fields[] = 'opposing_accounts.name as opposing_account_name';
$this->fields[] = 'opposing_accounts.encrypted as opposing_account_encrypted';
$this->fields[] = 'opposing_account_types.type as opposing_account_type';
$this->fields[] = 'opposing.account_id as opposing_account_id';
$this->fields[] = 'opposing_accounts.name as opposing_account_name';
$this->fields[] = 'opposing_accounts.encrypted as opposing_account_encrypted';
$this->fields[] = 'opposing_account_types.type as opposing_account_type';
$this->joinedOpposing = true;
Log::debug('joinedOpposing is now true!');
}
}
@ -622,17 +692,17 @@ class JournalCollector implements JournalCollectorInterface
{
$query = Transaction::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->leftJoin('transaction_currencies', 'transaction_currencies.id', 'transaction_journals.transaction_currency_id')
->leftJoin('transaction_types', 'transaction_types.id', 'transaction_journals.transaction_type_id')
->leftJoin('bills', 'bills.id', 'transaction_journals.bill_id')
->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id')
->leftJoin('account_types', 'accounts.account_type_id', 'account_types.id')
->whereNull('transactions.deleted_at')
->whereNull('transaction_journals.deleted_at')
->where('transaction_journals.user_id', $this->user->id)
->orderBy('transaction_journals.date', 'DESC')
->orderBy('transaction_journals.order', 'ASC')
->orderBy('transaction_journals.id', 'DESC');
->leftJoin('transaction_currencies', 'transaction_currencies.id', 'transaction_journals.transaction_currency_id')
->leftJoin('transaction_types', 'transaction_types.id', 'transaction_journals.transaction_type_id')
->leftJoin('bills', 'bills.id', 'transaction_journals.bill_id')
->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id')
->leftJoin('account_types', 'accounts.account_type_id', 'account_types.id')
->whereNull('transactions.deleted_at')
->whereNull('transaction_journals.deleted_at')
->where('transaction_journals.user_id', $this->user->id)
->orderBy('transaction_journals.date', 'DESC')
->orderBy('transaction_journals.order', 'ASC')
->orderBy('transaction_journals.id', 'DESC');
return $query;

View File

@ -38,6 +38,16 @@ interface JournalCollectorInterface
*/
public function disableFilter(): JournalCollectorInterface;
/**
* @return JournalCollectorInterface
*/
public function disableInternalFilter(): JournalCollectorInterface;
/**
* @return JournalCollectorInterface
*/
public function enableInternalFilter(): JournalCollectorInterface;
/**
* @return Collection
*/
@ -46,7 +56,7 @@ interface JournalCollectorInterface
/**
* @return LengthAwarePaginator
*/
public function getPaginatedJournals():LengthAwarePaginator;
public function getPaginatedJournals(): LengthAwarePaginator;
/**
* @param Collection $accounts

View File

@ -15,10 +15,15 @@ namespace FireflyIII\Http\Controllers\Report;
use Carbon\Carbon;
use FireflyIII\Helpers\Collector\JournalCollectorInterface;
use FireflyIII\Helpers\Report\ReportHelperInterface;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\Category;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Category\CategoryRepositoryInterface;
use FireflyIII\Support\CacheProperties;
use Illuminate\Support\Collection;
use Navigation;
/**
* Class CategoryController
@ -27,6 +32,84 @@ use Illuminate\Support\Collection;
*/
class CategoryController extends Controller
{
/**
*
* @param Carbon $start
* @param Carbon $end
* @param Collection $accounts
*
* @return string
*/
public function categoryPeriodReport(Carbon $start, Carbon $end, Collection $accounts)
{
/** @var CategoryRepositoryInterface $repository */
$repository = app(CategoryRepositoryInterface::class);
$categories = $repository->getCategories();
$data = [];
// income only:
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
$collector->setAccounts($accounts)->setRange($start, $end)
->withOpposingAccount()
->enableInternalFilter()
->setCategories($categories);
$transactions = $collector->getJournals();
// this is the date format we need:
// define period to group on:
$carbonFormat = Navigation::preferredCarbonFormat($start, $end);
// this is the set of transactions for this period
// in these budgets. Now they must be grouped (manually)
// id, period => amount
$income = [];
foreach ($transactions as $transaction) {
$categoryId = max(intval($transaction->transaction_journal_category_id), intval($transaction->transaction_category_id));
$date = $transaction->date->format($carbonFormat);
if (!isset($income[$categoryId])) {
$income[$categoryId]['name'] = $this->getCategoryName($categoryId, $categories);
$income[$categoryId]['sum'] = '0';
$income[$categoryId]['entries'] = [];
}
if (!isset($income[$categoryId]['entries'][$date])) {
$income[$categoryId]['entries'][$date] = '0';
}
$income[$categoryId]['entries'][$date] = bcadd($income[$categoryId]['entries'][$date], $transaction->transaction_amount);
}
// and now the same for stuff without a category:
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($start, $end);
$collector->setTypes([TransactionType::DEPOSIT, TransactionType::TRANSFER]);
$collector->withoutCategory();
$transactions = $collector->getJournals();
$income[0]['entries'] = [];
$income[0]['name'] = strval(trans('firefly.no_category'));
$income[0]['sum'] = '0';
foreach ($transactions as $transaction) {
$date = $transaction->date->format($carbonFormat);
if (!isset($income[0]['entries'][$date])) {
$income[0]['entries'][$date] = '0';
}
$income[0]['entries'][$date] = bcadd($income[0]['entries'][$date], $transaction->transaction_amount);
}
$periods = Navigation::listOfPeriods($start, $end);
$income = $this->filterCategoryPeriodReport($income);
$result = view('reports.partials.category-period', compact('categories', 'periods', 'income'))->render();
return $result;
}
/**
* @param ReportHelperInterface $helper
@ -56,4 +139,52 @@ class CategoryController extends Controller
return $result;
}
/**
* Filters empty results from category period report
*
* @param array $data
*
* @return array
*/
private function filterCategoryPeriodReport(array $data): array
{
/**
* @var int $categoryId
* @var array $set
*/
foreach ($data as $categoryId => $set) {
$sum = '0';
foreach ($set['entries'] as $amount) {
$sum = bcadd($amount, $sum);
}
$data[$categoryId]['sum'] = $sum;
if (bccomp('0', $sum) === 0) {
unset($data[$categoryId]);
}
}
return $data;
}
/**
* @param int $categoryId
* @param Collection $categories
*
* @return string
*/
private function getCategoryName(int $categoryId, Collection $categories): string
{
$first = $categories->filter(
function (Category $category) use ($categoryId) {
return $categoryId === $category->id;
}
);
if (!is_null($first->first())) {
return $first->first()->name;
}
return '(unknown)';
}
}

View File

@ -226,55 +226,34 @@ class BudgetRepository implements BudgetRepositoryInterface
*/
public function getBudgetPeriodReport(Collection $budgets, Collection $accounts, Carbon $start, Carbon $end): array
{
$carbonFormat = Navigation::preferredCarbonFormat($start, $end);
$data = [];
// prep data array:
/** @var Budget $budget */
foreach ($budgets as $budget) {
$data[$budget->id] = [
'name' => $budget->name,
'sum' => '0',
'entries' => [],
];
}
// get all transactions:
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($start, $end);
$collector->setAccounts($accounts)->setRange($start, $end);
$collector->setBudgets($budgets);
$transactions = $collector->getJournals();
// this is the date format we need:
// define period to group on:
$carbonFormat = Navigation::preferredCarbonFormat($start, $end);
// this is the set of transactions for this period
// in these budgets. Now they must be grouped (manually)
// id, period => amount
$data = [];
// loop transactions:
/** @var Transaction $transaction */
foreach ($transactions as $transaction) {
$budgetId = max(intval($transaction->transaction_journal_budget_id), intval($transaction->transaction_budget_id));
$date = $transaction->date->format($carbonFormat);
if (!isset($data[$budgetId])) {
$data[$budgetId]['name'] = $this->getBudgetName($budgetId, $budgets);
$data[$budgetId]['sum'] = '0';
$data[$budgetId]['entries'] = [];
}
if (!isset($data[$budgetId]['entries'][$date])) {
$data[$budgetId]['entries'][$date] = '0';
}
$data[$budgetId]['entries'][$date] = bcadd($data[$budgetId]['entries'][$date], $transaction->transaction_amount);
$budgetId = max(intval($transaction->transaction_journal_budget_id), intval($transaction->transaction_budget_id));
$date = $transaction->date->format($carbonFormat);
$data[$budgetId]['entries'][$date] = bcadd($data[$budgetId]['entries'][$date] ?? '0', $transaction->transaction_amount);
}
// and now the same for stuff without a budget:
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($start, $end);
$collector->setTypes([TransactionType::WITHDRAWAL]);
$collector->withoutBudget();
$transactions = $collector->getJournals();
$data[0]['entries'] = [];
$data[0]['name'] = strval(trans('firefly.no_budget'));
$data[0]['sum'] = '0';
foreach ($transactions as $transaction) {
$date = $transaction->date->format($carbonFormat);
if (!isset($data[0]['entries'][$date])) {
$data[0]['entries'][$date] = '0';
}
$data[0]['entries'][$date] = bcadd($data[0]['entries'][$date], $transaction->transaction_amount);
}
$data[0] = $this->getNoBudgetPeriodReport($start, $end);
return $data;
@ -332,19 +311,23 @@ class BudgetRepository implements BudgetRepositoryInterface
Log::debug('spentInPeriod: and these accounts: ', $accountIds);
Log::debug(sprintf('spentInPeriod: Start date is "%s", end date is "%s"', $start->format('Y-m-d'), $end->format('Y-m-d')));
$fromJournalsQuery = TransactionJournal::leftJoin('budget_transaction_journal', 'budget_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id')
->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
->leftJoin(
'transactions', function (JoinClause $join) {
$join->on('transactions.transaction_journal_id', '=', 'transaction_journals.id')->where('transactions.amount', '<', 0);
}
)
->where('transaction_journals.date', '>=', $start->format('Y-m-d'))
->where('transaction_journals.date', '<=', $end->format('Y-m-d'))
->whereNull('transaction_journals.deleted_at')
->whereNull('transactions.deleted_at')
->where('transaction_journals.user_id', $this->user->id)
->where('transaction_types.type', 'Withdrawal');
$fromJournalsQuery = TransactionJournal::leftJoin(
'budget_transaction_journal', 'budget_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id'
)
->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
->leftJoin(
'transactions', function (JoinClause $join) {
$join->on('transactions.transaction_journal_id', '=', 'transaction_journals.id')->where(
'transactions.amount', '<', 0
);
}
)
->where('transaction_journals.date', '>=', $start->format('Y-m-d'))
->where('transaction_journals.date', '<=', $end->format('Y-m-d'))
->whereNull('transaction_journals.deleted_at')
->whereNull('transactions.deleted_at')
->where('transaction_journals.user_id', $this->user->id)
->where('transaction_types.type', 'Withdrawal');
// add budgets:
if ($budgets->count() > 0) {
@ -368,15 +351,15 @@ class BudgetRepository implements BudgetRepositoryInterface
* and transactions.account_id in (2)
*/
$fromTransactionsQuery = Transaction::leftJoin('budget_transaction', 'budget_transaction.transaction_id', '=', 'transactions.id')
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
->whereNull('transactions.deleted_at')
->whereNull('transaction_journals.deleted_at')
->where('transactions.amount', '<', 0)
->where('transaction_journals.date', '>=', $start->format('Y-m-d'))
->where('transaction_journals.date', '<=', $end->format('Y-m-d'))
->where('transaction_journals.user_id', $this->user->id)
->where('transaction_types.type', 'Withdrawal');
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
->whereNull('transactions.deleted_at')
->whereNull('transaction_journals.deleted_at')
->where('transactions.amount', '<', 0)
->where('transaction_journals.date', '>=', $start->format('Y-m-d'))
->where('transaction_journals.date', '<=', $end->format('Y-m-d'))
->where('transaction_journals.user_id', $this->user->id)
->where('transaction_types.type', 'Withdrawal');
// add budgets:
if ($budgets->count() > 0) {
@ -569,4 +552,37 @@ class BudgetRepository implements BudgetRepositoryInterface
return '(unknown)';
}
/**
* @param Carbon $start
* @param Carbon $end
*
* @return array
*/
private function getNoBudgetPeriodReport(Carbon $start, Carbon $end): array
{
$carbonFormat = Navigation::preferredCarbonFormat($start, $end);
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($start, $end);
$collector->setTypes([TransactionType::WITHDRAWAL]);
$collector->withoutBudget();
$transactions = $collector->getJournals();
$result = [
'entries' => [],
'name' => strval(trans('firefly.no_budget')),
'sum' => '0',
];
foreach ($transactions as $transaction) {
$date = $transaction->date->format($carbonFormat);
if (!isset($result['entries'][$date])) {
$result['entries'][$date] = '0';
}
$result['entries'][$date] = bcadd($result['entries'][$date], $transaction->transaction_amount);
}
return $result;
}
}

View File

@ -246,8 +246,8 @@ class Navigation
* If the date difference between start and end is less than a month, method returns "Y-m-d". If the difference is less than a year,
* method returns "Y-m". If the date difference is larger, method returns "Y".
*
* @param Carbon $start
* @param Carbon $end
* @param \Carbon\Carbon $start
* @param \Carbon\Carbon $end
*
* @return string
*/
@ -270,8 +270,8 @@ class Navigation
* If the date difference between start and end is less than a month, method returns "1D". If the difference is less than a year,
* method returns "1M". If the date difference is larger, method returns "1Y".
*
* @param Carbon $start
* @param Carbon $end
* @param \Carbon\Carbon $start
* @param \Carbon\Carbon $end
*
* @return string
*/

View File

@ -15,7 +15,7 @@ var fixHelper = function (e, tr) {
$(function () {
"use strict";
lineChart('chart/account/' + accountID, 'overview-chart');
lineChart('chart/account/' + accountID, 'overview-chart');
pieChart(incomeByCategoryUri, 'account-cat-in');
pieChart(expenseByCategoryUri, 'account-cat-out');
pieChart(expenseByBudgetUri, 'account-budget-out');

View File

@ -1,10 +1,11 @@
/* globals google, accountIds, budgetPeriodReportUri */
/* globals google, accountIds, budgetPeriodReportUri, categoryPeriodReportUri */
$(function () {
"use strict";
drawChart();
loadAjaxPartial('budgetPeriodReport', budgetPeriodReportUri);
loadAjaxPartial('categoryPeriodReport', categoryPeriodReportUri);
});
function drawChart() {

View File

@ -113,6 +113,19 @@
</div>
</div>
{# same thing but for categories #}
<div class="row">
<div class="col-lg-12 col-md-12 col-sm-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">{{ 'categories'|_ }}</h3>
</div>
<div class="box-body no-padding table-responsive loading" id="categoryPeriodReport">
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script type="text/javascript" src="js/lib/bootstrap-sortable.js"></script>
@ -135,6 +148,7 @@
var incExpReportUri = '{{ route('reports.data.incExpReport', [start.format('Ymd'), end.format('Ymd'), accountIds]) }}';
var budgetPeriodReportUri = '{{ route('reports.data.budgetPeriodReport', [start.format('Ymd'), end.format('Ymd'), accountIds]) }}';
var categoryPeriodReportUri = '{{ route('reports.data.categoryPeriodReport', [start.format('Ymd'), end.format('Ymd'), accountIds]) }}';
</script>
<script type="text/javascript" src="js/ff/reports/default/all.js"></script>

View File

@ -0,0 +1,54 @@
<table class="table table-hover sortable">
<thead>
<tr>
<th rowspan="2" data-defaultsort="disabled">{{ 'category'|_ }}</th>
{% for period in periods %}
<th colspan="2" data-defaultsort="disabled">{{ period }}</th>
{% endfor %}
<th colspan="2" data-defaultsort="disabled">{{ 'sum'|_ }}</th>
</tr>
<tr>
{% for period in periods %}
<th data-defaultsign="_19">In</th>
<th data-defaultsign="_19">Out</th>
{% endfor %}
<th data-defaultsign="_19">In</th>
<th data-defaultsign="_19">Out</th>
</tr>
</thead>
<tbody>
{% for category in categories %}
{% if income[category.id] %}
<tr>
<td data-value="{{ category.name }}">
<a title="{{ category.name }}" href="#" data-category="{{ category.id }}" class="category-chart-activate">{{ category.name }}</a>
</td>
{% for key, period in periods %}
{# income first #}
{% if(income[category.id].entries[key]) %}
<td data-value="{{ income[category.id].entries[key] }}">
{{ income[category.id].entries[key]|formatAmount }}
</td>
{% else %}
<td data-value="0">
{{ 0|formatAmount }}
</td>
{% endif %}
{# expenses #}
<td data-value="0">
<!-- expense -->
</td>
{% endfor %}
<td data-value="{{ info.sum }}">
0
</td>
<td data-value="{{ info.sum }}">
1
</td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>

View File

@ -402,12 +402,19 @@ Route::group(
'/reports/data/budget-report/{start_date}/{end_date}/{accountList}',
['uses' => 'Report\BudgetController@budgetReport', 'as' => 'reports.data.budgetReport']
);
// budget period overview
Route::get(
'/reports/data/budget-period/{start_date}/{end_date}/{accountList}',
['uses' => 'Report\BudgetController@budgetPeriodReport', 'as' => 'reports.data.budgetPeriodReport']
);
// category period overview
Route::get(
'/reports/data/category-period/{start_date}/{end_date}/{accountList}',
['uses' => 'Report\CategoryController@categoryPeriodReport', 'as' => 'reports.data.categoryPeriodReport']
);
/**
* Rules Controller
*/