diff --git a/app/Helpers/Collector/JournalCollector.php b/app/Helpers/Collector/JournalCollector.php index 328f4ca53f..7816e1a61c 100644 --- a/app/Helpers/Collector/JournalCollector.php +++ b/app/Helpers/Collector/JournalCollector.php @@ -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; diff --git a/app/Helpers/Collector/JournalCollectorInterface.php b/app/Helpers/Collector/JournalCollectorInterface.php index a9a70a8944..320fcab93b 100644 --- a/app/Helpers/Collector/JournalCollectorInterface.php +++ b/app/Helpers/Collector/JournalCollectorInterface.php @@ -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 diff --git a/app/Http/Controllers/Report/CategoryController.php b/app/Http/Controllers/Report/CategoryController.php index 48ee5d77b7..3e993a9b16 100644 --- a/app/Http/Controllers/Report/CategoryController.php +++ b/app/Http/Controllers/Report/CategoryController.php @@ -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)'; + } + } \ No newline at end of file diff --git a/app/Repositories/Budget/BudgetRepository.php b/app/Repositories/Budget/BudgetRepository.php index c2db7da39a..d76c229ac3 100644 --- a/app/Repositories/Budget/BudgetRepository.php +++ b/app/Repositories/Budget/BudgetRepository.php @@ -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; + } } diff --git a/app/Support/Navigation.php b/app/Support/Navigation.php index f8ec640963..48ff2724fb 100644 --- a/app/Support/Navigation.php +++ b/app/Support/Navigation.php @@ -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 */ diff --git a/public/js/ff/accounts/show.js b/public/js/ff/accounts/show.js index 0bfb8e9a69..4de17eddb5 100644 --- a/public/js/ff/accounts/show.js +++ b/public/js/ff/accounts/show.js @@ -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'); diff --git a/public/js/ff/reports/default/year.js b/public/js/ff/reports/default/year.js index a7c5121abe..237f3bac77 100644 --- a/public/js/ff/reports/default/year.js +++ b/public/js/ff/reports/default/year.js @@ -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() { diff --git a/resources/views/reports/default/year.twig b/resources/views/reports/default/year.twig index 8ef09d32af..1824ad5f05 100644 --- a/resources/views/reports/default/year.twig +++ b/resources/views/reports/default/year.twig @@ -113,6 +113,19 @@ + {# same thing but for categories #} +
{{ 'category'|_ }} | + {% for period in periods %} +{{ period }} | + {% endfor %} +{{ 'sum'|_ }} | +|||
---|---|---|---|---|---|
In | +Out | + {% endfor %} +In | +Out | +||
+ {{ category.name }} + | + + {% for key, period in periods %} + {# income first #} + {% if(income[category.id].entries[key]) %} ++ {{ income[category.id].entries[key]|formatAmount }} + | + {% else %} ++ {{ 0|formatAmount }} + | + {% endif %} + + {# expenses #} ++ + | + {% endfor %} ++ 0 + | ++ 1 + | +