diff --git a/app/Helpers/Collector/JournalCollector.php b/app/Helpers/Collector/JournalCollector.php index 33621ad862..d719a74ee2 100644 --- a/app/Helpers/Collector/JournalCollector.php +++ b/app/Helpers/Collector/JournalCollector.php @@ -497,6 +497,20 @@ class JournalCollector implements JournalCollectorInterface return $this; } + /** + * @param Collection $accounts + * + * @return JournalCollectorInterface + */ + public function setOpposingAccounts(Collection $accounts): JournalCollectorInterface + { + $this->withOpposingAccount(); + + $this->query->whereIn('opposing.account_id', $accounts->pluck('id')->toArray()); + + return $this; + } + /** * @param int $page * diff --git a/app/Helpers/Collector/JournalCollectorInterface.php b/app/Helpers/Collector/JournalCollectorInterface.php index 5ac5e90bed..e23991e18c 100644 --- a/app/Helpers/Collector/JournalCollectorInterface.php +++ b/app/Helpers/Collector/JournalCollectorInterface.php @@ -85,6 +85,13 @@ interface JournalCollectorInterface */ public function removeFilter(string $filter): JournalCollectorInterface; + /** + * @param Collection $accounts + * + * @return JournalCollectorInterface + */ + public function setOpposingAccounts(Collection $accounts): JournalCollectorInterface; + /** * @param Collection $accounts * diff --git a/app/Http/Controllers/Report/ExpenseController.php b/app/Http/Controllers/Report/ExpenseController.php index fdf896db97..3b2e2002a0 100644 --- a/app/Http/Controllers/Report/ExpenseController.php +++ b/app/Http/Controllers/Report/ExpenseController.php @@ -24,10 +24,13 @@ declare(strict_types=1); namespace FireflyIII\Http\Controllers\Report; use Carbon\Carbon; +use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; +use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Support\CacheProperties; use Illuminate\Support\Collection; @@ -56,23 +59,150 @@ class ExpenseController extends Controller ); } + public function category(Collection $accounts, Collection $expense, Carbon $start, Carbon $end) + { + // Properties for cache: + $cache = new CacheProperties; + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty('expense-category'); + $cache->addProperty($accounts->pluck('id')->toArray()); + $cache->addProperty($expense->pluck('id')->toArray()); + if ($cache->has()) { + return $cache->get(); // @codeCoverageIgnore + } + $combined = $this->combineAccounts($expense); + $all = new Collection; + foreach ($combined as $name => $combi) { + $all = $all->merge($combi); + } + // now find spent / earned: + $spent = $this->spentByCategory($accounts, $all, $start, $end); + $earned = $this->earnedByCategory($accounts, $combi, $start, $end); + + // join arrays somehow: + $together = []; + foreach($spent as $categoryId => $spentInfo) { + if(!isset($together[$categoryId])) { + $together[$categoryId]['spent'] = $spentInfo; + // get category info: + $first = reset($spentInfo); + $together[$categoryId]['category'] = $first['category']; + } + } + + foreach($earned as $categoryId => $earnedInfo) { + if(!isset($together[$categoryId])) { + $together[$categoryId]['earned'] = $earnedInfo; + // get category info: + $first = reset($earnedInfo); + $together[$categoryId]['category'] = $first['category']; + } + } + + $result = view('reports.partials.exp-categories', compact('together'))->render(); + $cache->store($result); + + return $result; + } + /** * @param Collection $accounts * @param Collection $expense * @param Carbon $start * @param Carbon $end + * + * @return array|mixed|string + * @throws \Throwable + */ + public function spent(Collection $accounts, Collection $expense, Carbon $start, Carbon $end) + { + // chart properties for cache: + $cache = new CacheProperties; + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty('expense-spent'); + $cache->addProperty($accounts->pluck('id')->toArray()); + $cache->addProperty($expense->pluck('id')->toArray()); + if ($cache->has()) { + return $cache->get(); // @codeCoverageIgnore + } + + $combined = $this->combineAccounts($expense); + $result = []; + + foreach ($combined as $name => $combi) { + /** + * @var string $name + * @var Collection $combi + */ + $spent = $this->spentInPeriod($accounts, $combi, $start, $end); + $earned = $this->earnedInPeriod($accounts, $combi, $start, $end); + $result[$name] = [ + 'spent' => $spent, + 'earned' => $earned, + ]; + } + $result = view('reports.partials.exp-not-grouped', compact('result'))->render(); + $cache->store($result); + + return $result; + // for period, get spent and earned for each account (by name) + + } + + /** + * @param Collection $accounts + * @param Collection $expense + * @param Carbon $start + * @param Carbon $end + * + * @return array|mixed|string + * @throws \Throwable */ public function spentGrouped(Collection $accounts, Collection $expense, Carbon $start, Carbon $end) { - $combined = $this->combineAccounts($expense); - // for period, get spent and earned for each account (by name) - /** - * @var string $name - * @var Collection $combi - */ - foreach($combined as $name => $combi) { - + // Properties for cache: + $cache = new CacheProperties; + $cache->addProperty($start); + $cache->addProperty($end); + $cache->addProperty('expense-spent-grouped'); + $cache->addProperty($accounts->pluck('id')->toArray()); + $cache->addProperty($expense->pluck('id')->toArray()); + if ($cache->has()) { + return $cache->get(); // @codeCoverageIgnore } + + $combined = $this->combineAccounts($expense); + $format = app('navigation')->preferredRangeFormat($start, $end); + $result = []; + + foreach ($combined as $name => $combi) { + $current = clone $start; + $combiSet = []; + while ($current <= $end) { + $period = $current->format('Ymd'); + $periodName = app('navigation')->periodShow($current, $format); + $currentEnd = app('navigation')->endOfPeriod($current, $format); + /** + * @var string $name + * @var Collection $combi + */ + $spent = $this->spentInPeriod($accounts, $combi, $current, $currentEnd); + $earned = $this->earnedInPeriod($accounts, $combi, $current, $currentEnd); + $current = app('navigation')->addPeriod($current, $format, 0); + $combiSet[$period] = [ + 'period' => $periodName, + 'spent' => $spent, + 'earned' => $earned, + ]; + } + $result[$name] = $combiSet; + } + $result = view('reports.partials.exp-grouped', compact('result'))->render(); + $cache->store($result); + + return $result; } protected function combineAccounts(Collection $accounts): array @@ -80,14 +210,187 @@ class ExpenseController extends Controller $combined = []; /** @var Account $expenseAccount */ foreach ($accounts as $expenseAccount) { - $combined[$expenseAccount->name] = [$expenseAccount]; + $collection = new Collection; + $collection->push($expenseAccount); + $revenue = $this->accountRepository->findByName($expenseAccount->name, [AccountType::REVENUE]); if (!is_null($revenue->id)) { - $combined[$expenseAccount->name][] = $revenue; + $collection->push($revenue); } + $combined[$expenseAccount->name] = $collection; } return $combined; } + protected function earnedInPeriod(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array + { + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->setAccounts($assets); + $collector->setOpposingAccounts($opposing); + $set = $collector->getJournals(); + $sum = []; + // loop to support multi currency + foreach ($set as $transaction) { + $currencyId = $transaction->transaction_currency_id; + + // if not set, set to zero: + if (!isset($sum[$currencyId])) { + $sum[$currencyId] = [ + 'sum' => '0', + 'currency' => [ + 'symbol' => $transaction->transaction_currency_symbol, + 'dp' => $transaction->transaction_currency_dp, + ], + ]; + } + + // add amount + $sum[$currencyId]['sum'] = bcadd($sum[$currencyId]['sum'], $transaction->transaction_amount); + } + + return $sum; + } + + /** + * @param Collection $assets + * @param Collection $opposing + * @param Carbon $start + * @param Carbon $end + * + * @return string + */ + protected function earnedByCategory(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array + { + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setRange($start, $end)->setTypes([TransactionType::DEPOSIT])->setAccounts($assets); + $collector->setOpposingAccounts($opposing)->withCategoryInformation(); + $set = $collector->getJournals(); + $sum = []; + // loop to support multi currency + foreach ($set as $transaction) { + $currencyId = $transaction->transaction_currency_id; + $categoryName = $transaction->transaction_category_name; + $categoryId = intval($transaction->transaction_category_id); + // if null, grab from journal: + if ($categoryId === 0) { + $categoryName = $transaction->transaction_journal_category_name; + $categoryId = intval($transaction->transaction_journal_category_id); + } + if($categoryId !== 0) { + $categoryName = app('steam')->tryDecrypt($categoryName); + } + + // if not set, set to zero: + if (!isset($sum[$categoryId][$currencyId])) { + $sum[$categoryId][$currencyId] = [ + 'sum' => '0', + 'category' => [ + 'id' => $categoryId, + 'name' => $categoryName, + ], + 'currency' => [ + 'symbol' => $transaction->transaction_currency_symbol, + 'dp' => $transaction->transaction_currency_dp, + ], + ]; + } + + // add amount + $sum[$categoryId][$currencyId]['sum'] = bcadd($sum[$categoryId][$currencyId]['sum'], $transaction->transaction_amount); + } + + return $sum; + } + + /** + * @param Collection $assets + * @param Collection $opposing + * @param Carbon $start + * @param Carbon $end + * + * @return string + */ + protected function spentByCategory(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array + { + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setAccounts($assets); + $collector->setOpposingAccounts($opposing)->withCategoryInformation(); + $set = $collector->getJournals(); + $sum = []; + // loop to support multi currency + foreach ($set as $transaction) { + $currencyId = $transaction->transaction_currency_id; + $categoryName = $transaction->transaction_category_name; + $categoryId = intval($transaction->transaction_category_id); + // if null, grab from journal: + if ($categoryId === 0) { + $categoryName = $transaction->transaction_journal_category_name; + $categoryId = intval($transaction->transaction_journal_category_id); + } + + // if not set, set to zero: + if (!isset($sum[$categoryId][$currencyId])) { + $sum[$categoryId][$currencyId] = [ + 'sum' => '0', + 'category' => [ + 'id' => $categoryId, + 'name' => $categoryName, + ], + 'currency' => [ + 'symbol' => $transaction->transaction_currency_symbol, + 'dp' => $transaction->transaction_currency_dp, + ], + ]; + } + + // add amount + $sum[$categoryId][$currencyId]['sum'] = bcadd($sum[$categoryId][$currencyId]['sum'], $transaction->transaction_amount); + } + + return $sum; + } + + + /** + * @param Collection $assets + * @param Collection $opposing + * @param Carbon $start + * @param Carbon $end + * + * @return string + */ + protected function spentInPeriod(Collection $assets, Collection $opposing, Carbon $start, Carbon $end): array + { + /** @var JournalCollectorInterface $collector */ + $collector = app(JournalCollectorInterface::class); + $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setAccounts($assets); + $collector->setOpposingAccounts($opposing); + $set = $collector->getJournals(); + $sum = []; + // loop to support multi currency + foreach ($set as $transaction) { + $currencyId = $transaction->transaction_currency_id; + + // if not set, set to zero: + if (!isset($sum[$currencyId])) { + $sum[$currencyId] = [ + 'sum' => '0', + 'currency' => [ + 'symbol' => $transaction->transaction_currency_symbol, + 'dp' => $transaction->transaction_currency_dp, + ], + ]; + } + + // add amount + $sum[$currencyId]['sum'] = bcadd($sum[$currencyId]['sum'], $transaction->transaction_amount); + } + + return $sum; + } + } \ No newline at end of file diff --git a/public/js/ff/reports/account/all.js b/public/js/ff/reports/account/all.js index 81a80886b1..539b599256 100644 --- a/public/js/ff/reports/account/all.js +++ b/public/js/ff/reports/account/all.js @@ -25,4 +25,33 @@ function loadAjaxPartial(holder, uri) { }).fail(function () { failAjaxPartial(uri, holder); }); +} + +function failAjaxPartial(uri, holder) { + "use strict"; + var holderObject = $('#' + holder); + holderObject.parent().find('.overlay').remove(); + holderObject.addClass('general-chart-error'); + +} + +function displayAjaxPartial(data, holder) { + "use strict"; + var obj = $('#' + holder); + obj.html(data); + obj.parent().find('.overlay').remove(); + + // call some often needed recalculations and what-not: + + // find a sortable table and make it sortable: + if (typeof $.bootstrapSortable === "function") { + $.bootstrapSortable(true); + } + + // find the info click things and respond to them: + triggerInfoClick(); + + // trigger list thing + listLengthInitial(); + } \ No newline at end of file diff --git a/resources/views/reports/partials/exp-categories.twig b/resources/views/reports/partials/exp-categories.twig new file mode 100644 index 0000000000..8934902b14 --- /dev/null +++ b/resources/views/reports/partials/exp-categories.twig @@ -0,0 +1,36 @@ + + + + + + + + + + {% for categoryId, entry in together %} + + + + + + {% endfor %} + +
{{ 'category'|_ }}{{ 'spent'|_ }}{{ 'earned'|_ }}
+ {% if entry.category.name|length ==0 %}{{ 'noCategory'|_ }}{% else %}{{ entry.category.name }}{% endif %} + + {% if entry.spent|length ==0 %} + {{ '0'|formatAmount }} + {% else %} + {% for expense in entry.spent %} + {{ formatAmountBySymbol(expense.sum, expense.currency.symbol, expense.currency.dp) }}
+ {% endfor %} + {% endif %} +
+ {% if entry.earned|length ==0 %} + {{ '0'|formatAmount }} + {% else %} + {% for income in entry.earned %} + {{ formatAmountBySymbol(income.sum, income.currency.symbol, income.currency.dp) }}
+ {% endfor %} + {% endif %} +
diff --git a/resources/views/reports/partials/exp-grouped.twig b/resources/views/reports/partials/exp-grouped.twig new file mode 100644 index 0000000000..b2bb66271f --- /dev/null +++ b/resources/views/reports/partials/exp-grouped.twig @@ -0,0 +1,37 @@ + + + + + + + + + + + {% for name, periods in result %} + {% for periodIndex, period in periods %} + + + + + + + {% endfor %} + {% endfor %} + + +
{{ 'name'|_ }}{{ 'period'|_ }}
{{ name }}{{ period.period }} + {% if period.spent|length == 0%} + {{ '0'|formatAmount }} + {% endif %} + {% for expense in period.spent %} + {{ formatAmountBySymbol(expense.sum, expense.currency.symbol, expense.currency.dp) }}
+ {% endfor %} +
+ {% if period.earned|length == 0 %} + {{ '0'|formatAmount }} + {% endif %} + {% for income in period.earned %} + {{ formatAmountBySymbol(income.sum, income.currency.symbol, income.currency.dp) }}
+ {% endfor %} +
diff --git a/resources/views/reports/partials/exp-not-grouped.twig b/resources/views/reports/partials/exp-not-grouped.twig new file mode 100644 index 0000000000..7a231112f3 --- /dev/null +++ b/resources/views/reports/partials/exp-not-grouped.twig @@ -0,0 +1,33 @@ + + + + + + + + + + {% for name, amounts in result %} + + + + + + {% endfor %} + + +
{{ 'name'|_ }}
{{ name }} + {% if amounts.spent|length == 0%} + {{ '0'|formatAmount }} + {% endif %} + {% for expense in amounts.spent %} + {{ formatAmountBySymbol(expense.sum, expense.currency.symbol, expense.currency.dp) }}
+ {% endfor %} +
+ {% if amounts.earned|length == 0 %} + {{ '0'|formatAmount }} + {% endif %} + {% for income in amounts.earned %} + {{ formatAmountBySymbol(income.sum, income.currency.symbol, income.currency.dp) }}
+ {% endfor %} +