diff --git a/app/Helpers/Collector/TransactionCollector.php b/app/Helpers/Collector/TransactionCollector.php index cb06889b14..680ff6c944 100644 --- a/app/Helpers/Collector/TransactionCollector.php +++ b/app/Helpers/Collector/TransactionCollector.php @@ -87,6 +87,7 @@ class TransactionCollector implements TransactionCollectorInterface 'transactions.amount as transaction_amount', 'transactions.transaction_currency_id as transaction_currency_id', + 'transaction_currencies.name as transaction_currency_name', 'transaction_currencies.code as transaction_currency_code', 'transaction_currencies.symbol as transaction_currency_symbol', 'transaction_currencies.decimal_places as transaction_currency_dp', diff --git a/app/Http/Controllers/Chart/AccountController.php b/app/Http/Controllers/Chart/AccountController.php index c4a481b655..3511aaf728 100644 --- a/app/Http/Controllers/Chart/AccountController.php +++ b/app/Http/Controllers/Chart/AccountController.php @@ -18,7 +18,6 @@ * You should have received a copy of the GNU General Public License * along with Firefly III. If not, see . */ -/** @noinspection NullPointerExceptionInspection */ declare(strict_types=1); namespace FireflyIII\Http\Controllers\Chart; @@ -30,6 +29,7 @@ use FireflyIII\Http\Controllers\Controller; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; @@ -54,24 +54,41 @@ class AccountController extends Controller /** @var GeneratorInterface Chart generation methods. */ protected $generator; + /** @var AccountRepositoryInterface Account repository. */ + private $accountRepository; + + /** @var CurrencyRepositoryInterface */ + private $currencyRepository; + /** * AccountController constructor. */ public function __construct() { parent::__construct(); - $this->generator = app(GeneratorInterface::class); + + $this->middleware( + function ($request, $next) { + $this->generator = app(GeneratorInterface::class); + $this->accountRepository = app(AccountRepositoryInterface::class); + $this->currencyRepository = app(CurrencyRepositoryInterface::class); + + return $next($request); + } + ); + + } /** * Shows the balances for all the user's expense accounts (on the front page). * - * @param AccountRepositoryInterface $repository + * This chart is (multi) currency aware. * * @return JsonResponse */ - public function expenseAccounts(AccountRepositoryInterface $repository): JsonResponse + public function expenseAccounts(): JsonResponse { /** @var Carbon $start */ $start = clone session('start', Carbon::now()->startOfMonth()); @@ -86,49 +103,70 @@ class AccountController extends Controller } $start->subDay(); - /** @var CurrencyRepositoryInterface $currencyRepos */ - $currencyRepos = app(CurrencyRepositoryInterface::class); - $currencies = []; - $chartData = [ - [ - 'label' => (string)trans('firefly.spent'), - 'type' => 'bar', - 'currency_symbol' => '¤', - 'backgroundColor' => 'rgba(219, 68, 55, 0.5)', // red - 'entries' => [], - ], - ]; - $accounts = $repository->getAccountsByType([AccountType::EXPENSE, AccountType::BENEFICIARY]); - $accountNames = $this->extractNames($accounts); + // prep some vars: + $currencies = []; + $chartData = []; + $tempData = []; + + // grab all accounts and names + $accounts = $this->accountRepository->getAccountsByType([AccountType::EXPENSE, AccountType::BENEFICIARY]); + $accountNames = $this->extractNames($accounts); + + // grab all balances $startBalances = app('steam')->balancesPerCurrencyByAccounts($accounts, $start); $endBalances = app('steam')->balancesPerCurrencyByAccounts($accounts, $end); - $tempData = []; - + // loop the end balances. This is an array for each account ($expenses) foreach ($endBalances as $accountId => $expenses) { $accountId = (int)$accountId; + // loop each expense entry (each entry can be a different currency). foreach ($expenses as $currencyId => $endAmount) { - $currencyId = (int)$currencyId; + $currencyId = (int)$currencyId; + + // see if there is an accompanying start amount. + // grab the difference and find the currency. $startAmount = $startBalances[$accountId][$currencyId] ?? '0'; $diff = bcsub($endAmount, $startAmount); - $currencies[$currencyId] = $currencies[$currencyId] ?? $currencyRepos->findNull($currencyId); - $title = (string)trans( - 'firefly.account_in_currency', ['account' => $accountNames[$accountId], 'currency' => $currencies[$currencyId]->name] - ); - $tempData[$title] = $diff; + $currencies[$currencyId] = $currencies[$currencyId] ?? $this->currencyRepository->findNull($currencyId); + if (0 !== bccomp($diff, '0')) { + // store the values in a temporary array. + $tempData[] = [ + 'name' => $accountNames[$accountId], + 'difference' => $diff, + 'diff_float' => (float)$diff, + 'currency_id' => $currencyId, + ]; + } } } - arsort($tempData, SORT_NUMERIC); - foreach ($tempData as $label => $entry) { - if (0 !== bccomp($entry, '0')) { - $chartData[0]['entries'][$label] = $entry; - } + // sort temp array by amount. + $amounts = array_column($tempData, 'diff_float'); + array_multisort($amounts, SORT_DESC, $tempData); + + // loop all found currencies and build the data array for the chart. + /** + * @var int $currencyId + * @var TransactionCurrency $currency + */ + foreach ($currencies as $currencyId => $currency) { + $dataSet + = [ + 'label' => (string)trans('firefly.spent'), + 'type' => 'bar', + 'currency_symbol' => $currency->symbol, + 'entries' => $this->expandNames($tempData), + ]; + $chartData[$currencyId] = $dataSet; } - if (1 === \count($currencies)) { - $first = array_first($currencies); - $chartData[0]['currency_symbol'] = $first->symbol; + + // loop temp data and place data in correct array: + foreach ($tempData as $entry) { + $currencyId = $entry['currency_id']; + $name = $entry['name']; + $chartData[$currencyId]['entries'][$name] = $entry['difference']; } + $data = $this->generator->multiSet($chartData); $cache->store($data); @@ -161,19 +199,31 @@ class AccountController extends Controller $transactions = $collector->getTransactions(); $chartData = []; $result = []; - + $budgetIds = []; /** @var Transaction $transaction */ foreach ($transactions as $transaction) { - $jrnlBudgetId = (int)$transaction->transaction_journal_budget_id; - $transBudgetId = (int)$transaction->transaction_budget_id; - $budgetId = max($jrnlBudgetId, $transBudgetId); - $result[$budgetId] = $result[$budgetId] ?? '0'; - $result[$budgetId] = bcadd($transaction->transaction_amount, $result[$budgetId]); + $jrnlBudgetId = (int)$transaction->transaction_journal_budget_id; + $transBudgetId = (int)$transaction->transaction_budget_id; + $currencyName = $transaction->transaction_currency_name; + $budgetId = max($jrnlBudgetId, $transBudgetId); + $combi = $budgetId . $currencyName; + $budgetIds[] = $budgetId; + if (!isset($result[$combi])) { + $result[$combi] = [ + 'total' => '0', + 'budget_id' => $budgetId, + 'currency' => $currencyName, + ]; + } + $result[$combi]['total'] = bcadd($transaction->transaction_amount, $result[$combi]['total']); } - $names = $this->getBudgetNames(array_keys($result)); - foreach ($result as $budgetId => $amount) { - $chartData[$names[$budgetId]] = $amount; + $names = $this->getBudgetNames($budgetIds); + foreach ($result as $row) { + $budgetId = $row['budget_id']; + $name = $names[$budgetId]; + $label = (string)trans('firefly.name_in_currency', ['name' => $name, 'currency' => $row['currency']]); + $chartData[$label] = $row['total']; } $data = $this->generator->pieChart($chartData); @@ -225,18 +275,32 @@ class AccountController extends Controller $transactions = $collector->getTransactions(); $result = []; $chartData = []; + $categoryIds = []; /** @var Transaction $transaction */ foreach ($transactions as $transaction) { - $jrnlCatId = (int)$transaction->transaction_journal_category_id; - $transCatId = (int)$transaction->transaction_category_id; - $categoryId = max($jrnlCatId, $transCatId); - $result[$categoryId] = $result[$categoryId] ?? '0'; - $result[$categoryId] = bcadd($transaction->transaction_amount, $result[$categoryId]); + $jrnlCatId = (int)$transaction->transaction_journal_category_id; + $transCatId = (int)$transaction->transaction_category_id; + $currencyName = $transaction->transaction_currency_name; + $categoryId = max($jrnlCatId, $transCatId); + $combi = $categoryId . $currencyName; + $categoryIds[] = $categoryId; + if (!isset($result[$combi])) { + $result[$combi] = [ + 'total' => '0', + 'category_id' => $categoryId, + 'currency' => $currencyName, + ]; + } + $result[$combi]['total'] = bcadd($transaction->transaction_amount, $result[$combi]['total']); } $names = $this->getCategoryNames(array_keys($result)); - foreach ($result as $categoryId => $amount) { - $chartData[$names[$categoryId]] = $amount; + + foreach ($result as $row) { + $categoryId = $row['category_id']; + $name = $names[$categoryId]; + $label = (string)trans('firefly.name_in_currency', ['name' => $name, 'currency' => $row['currency']]); + $chartData[$label] = $row['total']; } $data = $this->generator->pieChart($chartData); @@ -265,6 +329,8 @@ class AccountController extends Controller /** * Shows the balances for all the user's frontpage accounts. * + * TODO this chart is not multi-currency aware. + * * @param AccountRepositoryInterface $repository * * @return JsonResponse @@ -319,16 +385,28 @@ class AccountController extends Controller $chartData = []; /** @var Transaction $transaction */ foreach ($transactions as $transaction) { - $jrnlCatId = (int)$transaction->transaction_journal_category_id; - $transCatId = (int)$transaction->transaction_category_id; - $categoryId = max($jrnlCatId, $transCatId); - $result[$categoryId] = $result[$categoryId] ?? '0'; - $result[$categoryId] = bcadd($transaction->transaction_amount, $result[$categoryId]); + $jrnlCatId = (int)$transaction->transaction_journal_category_id; + $transCatId = (int)$transaction->transaction_category_id; + $categoryId = max($jrnlCatId, $transCatId); + $currencyName = $transaction->transaction_currency_name; + $combi = $categoryId . $currencyName; + $categoryIds[] = $categoryId; + if (!isset($result[$combi])) { + $result[$combi] = [ + 'total' => '0', + 'category_id' => $categoryId, + 'currency' => $currencyName, + ]; + } + $result[$combi]['total'] = bcadd($transaction->transaction_amount, $result[$combi]['total']); } $names = $this->getCategoryNames(array_keys($result)); - foreach ($result as $categoryId => $amount) { - $chartData[$names[$categoryId]] = $amount; + foreach ($result as $row) { + $categoryId = $row['category_id']; + $name = $names[$categoryId]; + $label = (string)trans('firefly.name_in_currency', ['name' => $name, 'currency' => $row['currency']]); + $chartData[$label] = $row['total']; } $data = $this->generator->pieChart($chartData); $cache->store($data); @@ -356,6 +434,8 @@ class AccountController extends Controller /** * Shows overview of account during a single period. * + * TODO this chart is not multi-currency aware. + * * @param Account $account * @param Carbon $start * @@ -415,6 +495,8 @@ class AccountController extends Controller /** * Shows the balances for a given set of dates and accounts. * + * TODO this chart is not multi-currency aware. + * * @param Carbon $start * @param Carbon $end * @param Collection $accounts @@ -430,41 +512,90 @@ class AccountController extends Controller /** * Shows the balances for all the user's revenue accounts. * - * @param AccountRepositoryInterface $repository + * This chart is multi-currency aware. * * @return JsonResponse */ - public function revenueAccounts(AccountRepositoryInterface $repository): JsonResponse + public function revenueAccounts(): JsonResponse { - $start = clone session('start', Carbon::now()->startOfMonth()); - $end = clone session('end', Carbon::now()->endOfMonth()); - $chartData = []; - $cache = new CacheProperties; + /** @var Carbon $start */ + $start = clone session('start', Carbon::now()->startOfMonth()); + /** @var Carbon $end */ + $end = clone session('end', Carbon::now()->endOfMonth()); + $cache = new CacheProperties; $cache->addProperty($start); $cache->addProperty($end); $cache->addProperty('chart.account.revenue-accounts'); if ($cache->has()) { - return response()->json($cache->get()); // @codeCoverageIgnore + //return response()->json($cache->get()); // @codeCoverageIgnore } - $accounts = $repository->getAccountsByType([AccountType::REVENUE]); - $start->subDay(); - $startBalances = app('steam')->balancesByAccounts($accounts, $start); - $endBalances = app('steam')->balancesByAccounts($accounts, $end); - foreach ($accounts as $account) { - $id = $account->id; - $startBalance = $startBalances[$id] ?? '0'; - $endBalance = $endBalances[$id] ?? '0'; - $diff = bcsub($endBalance, $startBalance); - $diff = bcmul($diff, '-1'); - if (0 !== bccomp($diff, '0')) { - $chartData[$account->name] = $diff; + // prep some vars: + $currencies = []; + $chartData = []; + $tempData = []; + + // grab all accounts and names + $accounts = $this->accountRepository->getAccountsByType([AccountType::REVENUE]); + $accountNames = $this->extractNames($accounts); + + // grab all balances + $startBalances = app('steam')->balancesPerCurrencyByAccounts($accounts, $start); + $endBalances = app('steam')->balancesPerCurrencyByAccounts($accounts, $end); + + // loop the end balances. This is an array for each account ($expenses) + foreach ($endBalances as $accountId => $expenses) { + $accountId = (int)$accountId; + // loop each expense entry (each entry can be a different currency). + foreach ($expenses as $currencyId => $endAmount) { + $currencyId = (int)$currencyId; + + // see if there is an accompanying start amount. + // grab the difference and find the currency. + $startAmount = $startBalances[$accountId][$currencyId] ?? '0'; + $diff = bcsub($endAmount, $startAmount); + $currencies[$currencyId] = $currencies[$currencyId] ?? $this->currencyRepository->findNull($currencyId); + if (0 !== bccomp($diff, '0')) { + // store the values in a temporary array. + $tempData[] = [ + 'name' => $accountNames[$accountId], + 'difference' => $diff, + 'diff_float' => (float)$diff, + 'currency_id' => $currencyId, + ]; + } } } - arsort($chartData); - $data = $this->generator->singleSet((string)trans('firefly.earned'), $chartData); + // sort temp array by amount. + $amounts = array_column($tempData, 'diff_float'); + array_multisort($amounts, SORT_DESC, $tempData); + + // loop all found currencies and build the data array for the chart. + /** + * @var int $currencyId + * @var TransactionCurrency $currency + */ + foreach ($currencies as $currencyId => $currency) { + $dataSet + = [ + 'label' => (string)trans('firefly.earned'), + 'type' => 'bar', + 'currency_symbol' => $currency->symbol, + 'entries' => $this->expandNames($tempData), + ]; + $chartData[$currencyId] = $dataSet; + } + + // loop temp data and place data in correct array: + foreach ($tempData as $entry) { + $currencyId = $entry['currency_id']; + $name = $entry['name']; + $chartData[$currencyId]['entries'][$name] = bcmul($entry['difference'], '-1'); + } + + $data = $this->generator->multiSet($chartData); $cache->store($data); return response()->json($data); @@ -535,6 +666,25 @@ class AccountController extends Controller } /** + * Small helper function for the revenue and expense account charts. + * + * @param array $names + * + * @return array + */ + private function expandNames(array $names): array + { + $result = []; + foreach ($names as $entry) { + $result[$entry['name']] = 0; + } + + return $result; + } + + /** + * Small helper function for the revenue and expense account charts. + * * @param Collection $accounts * * @return array @@ -549,36 +699,4 @@ class AccountController extends Controller return $return; } - - /** - * This method extracts the unique currency ID's from an array of balances. - * - * The given array is expected to be in this format: - * - * accountID1: - * currencyID1: balance - * currencyID2: balance - * accountID2: - * currencyID1: balance - * currencyID2: balance - * - * - * @param array $balances - * - * @return array - */ - private function getCurrencyIDs(array $balances): array - { - $currencies = []; - /** - * @var int $accountId - * @var array $info - */ - foreach ($balances as $accountId => $info) { - $currencies = array_merge(array_keys($info)); - } - - return array_unique($currencies); - } - } diff --git a/app/Http/Controllers/Chart/BillController.php b/app/Http/Controllers/Chart/BillController.php index 9936e2125a..0287184d7b 100644 --- a/app/Http/Controllers/Chart/BillController.php +++ b/app/Http/Controllers/Chart/BillController.php @@ -54,6 +54,8 @@ class BillController extends Controller /** * Shows all bills and whether or not they've been paid this month (pie chart). * + * TODO this chart is not multi-currency aware. + * * @param BillRepositoryInterface $repository * * @return JsonResponse @@ -88,9 +90,11 @@ class BillController extends Controller * Shows overview for a single bill. * * @param TransactionCollectorInterface $collector - * @param Bill $bill + * @param Bill $bill * * @return JsonResponse + * + * TODO this chart is not multi-currency aware. */ public function single(TransactionCollectorInterface $collector, Bill $bill): JsonResponse { @@ -109,9 +113,9 @@ class BillController extends Controller } ); $chartData = [ - ['type' => 'bar', 'label' => (string)trans('firefly.min-amount'),'currency_symbol' => $bill->transactionCurrency->symbol, 'entries' => []], - ['type' => 'bar', 'label' => (string)trans('firefly.max-amount'),'currency_symbol' => $bill->transactionCurrency->symbol, 'entries' => []], - ['type' => 'line', 'label' => (string)trans('firefly.journal-amount'),'currency_symbol' => $bill->transactionCurrency->symbol, 'entries' => []], + ['type' => 'bar', 'label' => (string)trans('firefly.min-amount'), 'currency_symbol' => $bill->transactionCurrency->symbol, 'entries' => []], + ['type' => 'bar', 'label' => (string)trans('firefly.max-amount'), 'currency_symbol' => $bill->transactionCurrency->symbol, 'entries' => []], + ['type' => 'line', 'label' => (string)trans('firefly.journal-amount'), 'currency_symbol' => $bill->transactionCurrency->symbol, 'entries' => []], ]; /** @var Transaction $entry */ diff --git a/app/Http/Controllers/Chart/BudgetController.php b/app/Http/Controllers/Chart/BudgetController.php index 0fc9f7a64f..639e56f1e8 100644 --- a/app/Http/Controllers/Chart/BudgetController.php +++ b/app/Http/Controllers/Chart/BudgetController.php @@ -75,6 +75,8 @@ class BudgetController extends Controller /** * Shows overview of a single budget. * + * TODO this chart is not multi-currency aware. + * * @param Budget $budget * * @return JsonResponse @@ -124,6 +126,8 @@ class BudgetController extends Controller /** * Shows the amount left in a specific budget limit. * + * TODO this chart is not multi-currency aware. + * * @param Budget $budget * @param BudgetLimit $budgetLimit * @@ -171,6 +175,8 @@ class BudgetController extends Controller /** * Shows how much is spent per asset account. * + * TODO this chart is not multi-currency aware. + * * @param Budget $budget * @param BudgetLimit|null $budgetLimit * @@ -221,6 +227,8 @@ class BudgetController extends Controller /** * Shows how much is spent per category. * + * TODO this chart is not multi-currency aware. + * * @param Budget $budget * @param BudgetLimit|null $budgetLimit * @@ -272,6 +280,8 @@ class BudgetController extends Controller /** * Shows how much is spent per expense account. * + * TODO this chart is not multi-currency aware. + * * @param Budget $budget * @param BudgetLimit|null $budgetLimit * @@ -323,6 +333,8 @@ class BudgetController extends Controller /** * Shows a budget list with spent/left/overspent. * + * TODO this chart is not multi-currency aware. + * * @return JsonResponse * * @SuppressWarnings(PHPMD.CyclomaticComplexity) @@ -378,6 +390,8 @@ class BudgetController extends Controller /** * Shows a budget overview chart (spent and budgeted). * + * TODO this chart is not multi-currency aware. + * * @param Budget $budget * @param Carbon $start * @param Carbon $end @@ -424,6 +438,8 @@ class BudgetController extends Controller /** * Shows a chart for transactions without a budget. * + * TODO this chart is not multi-currency aware. + * * @param Collection $accounts * @param Carbon $start * @param Carbon $end diff --git a/app/Http/Controllers/Chart/BudgetReportController.php b/app/Http/Controllers/Chart/BudgetReportController.php index dfe4e4a3ed..3879ff540c 100644 --- a/app/Http/Controllers/Chart/BudgetReportController.php +++ b/app/Http/Controllers/Chart/BudgetReportController.php @@ -69,6 +69,8 @@ class BudgetReportController extends Controller /** * Chart that groups expenses by the account. * + * TODO this chart is not multi-currency aware. + * * @param Collection $accounts * @param Collection $budgets * @param Carbon $start @@ -98,6 +100,8 @@ class BudgetReportController extends Controller /** * Chart that groups the expenses by budget. * + * TODO this chart is not multi-currency aware. + * * @param Collection $accounts * @param Collection $budgets * @param Carbon $start @@ -127,6 +131,8 @@ class BudgetReportController extends Controller /** * Main overview of a budget in the budget report. * + * TODO this chart is not multi-currency aware. + * * @param Collection $accounts * @param Collection $budgets * @param Carbon $start diff --git a/app/Http/Controllers/Chart/CategoryController.php b/app/Http/Controllers/Chart/CategoryController.php index 14955e6413..7e6a17eefd 100644 --- a/app/Http/Controllers/Chart/CategoryController.php +++ b/app/Http/Controllers/Chart/CategoryController.php @@ -27,8 +27,10 @@ use FireflyIII\Generator\Chart\Basic\GeneratorInterface; use FireflyIII\Http\Controllers\Controller; use FireflyIII\Models\AccountType; use FireflyIII\Models\Category; +use FireflyIII\Models\TransactionCurrency; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Category\CategoryRepositoryInterface; +use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; use FireflyIII\Support\CacheProperties; use Illuminate\Http\JsonResponse; use Illuminate\Support\Collection; @@ -55,6 +57,8 @@ class CategoryController extends Controller /** * Show an overview for a category for all time, per month/week/year. * + * TODO this chart is not multi-currency aware. + * * @param CategoryRepositoryInterface $repository * @param AccountRepositoryInterface $accountRepository * @param Category $category @@ -131,35 +135,82 @@ class CategoryController extends Controller $cache->addProperty($end); $cache->addProperty('chart.category.frontpage'); if ($cache->has()) { - return response()->json($cache->get()); // @codeCoverageIgnore + //return response()->json($cache->get()); // @codeCoverageIgnore } + + // currency repos: + /** @var CurrencyRepositoryInterface $currencyRepository */ + $currencyRepository = app(CurrencyRepositoryInterface::class); + $currencies = []; + + $chartData = []; + $tempData = []; $categories = $repository->getCategories(); $accounts = $accountRepository->getAccountsByType([AccountType::ASSET, AccountType::DEFAULT]); + /** @var Category $category */ foreach ($categories as $category) { - $spent = $repository->spentInPeriod(new Collection([$category]), $accounts, $start, $end); - if (bccomp($spent, '0') === -1) { - $chartData[$category->name] = bcmul($spent, '-1'); + $spentArray = $repository->spentInPeriodPerCurrency(new Collection([$category]), $accounts, $start, $end); + foreach ($spentArray as $currencyId => $spent) { + if (bccomp($spent, '0') === -1) { + $currencies[$currencyId] = $currencies[$currencyId] ?? $currencyRepository->findNull($currencyId); + $tempData[] = [ + 'name' => $category->name, + 'spent' => bcmul($spent, '-1'), + 'spent_float' => (float)bcmul($spent, '-1'), + 'currency_id' => $currencyId, + ]; + } } } + // no category per currency: + $noCategory = $repository->spentInPeriodPcWoCategory(new Collection, $start, $end); + foreach ($noCategory as $currencyId => $spent) { + $currencies[$currencyId] = $currencies[$currencyId] ?? $currencyRepository->findNull($currencyId); + $tempData[] = [ + 'name' => trans('firefly.no_category'), + 'spent' => bcmul($spent, '-1'), + 'spent_float' => (float)bcmul($spent, '-1'), + 'currency_id' => $currencyId, + ]; + } - $chartData[(string)trans('firefly.no_category')] = bcmul($repository->spentInPeriodWithoutCategory(new Collection, $start, $end), '-1'); + // sort temp array by amount. + $amounts = array_column($tempData, 'spent_float'); + array_multisort($amounts, SORT_DESC, $tempData); - // sort - arsort($chartData); - - $data = $this->generator->singleSet((string)trans('firefly.spent'), $chartData); + // loop all found currencies and build the data array for the chart. + /** + * @var int $currencyId + * @var TransactionCurrency $currency + */ + foreach ($currencies as $currencyId => $currency) { + $dataSet = [ + 'label' => (string)trans('firefly.spent'), + 'type' => 'bar', + 'currency_symbol' => $currency->symbol, + 'entries' => $this->expandNames($tempData), + ]; + $chartData[$currencyId] = $dataSet; + } + // loop temp data and place data in correct array: + foreach ($tempData as $entry) { + $currencyId = $entry['currency_id']; + $name = $entry['name']; + $chartData[$currencyId]['entries'][$name] = $entry['spent']; + } + $data = $this->generator->multiSet($chartData); $cache->store($data); return response()->json($data); } - - /** @noinspection MoreThanThreeArgumentsInspection */ /** * Chart report. * + * TODO this chart is not multi-currency aware. + * * @param Category $category * @param Collection $accounts * @param Carbon $start @@ -222,9 +273,13 @@ class CategoryController extends Controller } + /** @noinspection MoreThanThreeArgumentsInspection */ + /** * Chart for period for transactions without a category. * + * TODO this chart is not multi-currency aware. + * * @param Collection $accounts * @param Carbon $start * @param Carbon $end @@ -286,6 +341,8 @@ class CategoryController extends Controller /** * Chart for a specific period. * + * TODO this chart is not multi-currency aware. + * * @param Category $category * @param $date * @@ -301,10 +358,10 @@ class CategoryController extends Controller return response()->json($data); } - /** * Chart for a specific period (start and end). * + * * @param Category $category * @param Carbon $start * @param Carbon $end @@ -371,4 +428,21 @@ class CategoryController extends Controller return $data; } + + /** + * Small helper function for the revenue and expense account charts. + * + * @param array $names + * + * @return array + */ + private function expandNames(array $names): array + { + $result = []; + foreach ($names as $entry) { + $result[$entry['name']] = 0; + } + + return $result; + } } diff --git a/app/Http/Controllers/Chart/CategoryReportController.php b/app/Http/Controllers/Chart/CategoryReportController.php index 1cee0797c9..4ff0e3b465 100644 --- a/app/Http/Controllers/Chart/CategoryReportController.php +++ b/app/Http/Controllers/Chart/CategoryReportController.php @@ -64,6 +64,8 @@ class CategoryReportController extends Controller /** * Chart for expenses grouped by expense account. * + * TODO this chart is not multi-currency aware. + * * @param Collection $accounts * @param Collection $categories * @param Carbon $start @@ -90,6 +92,8 @@ class CategoryReportController extends Controller /** * Chart for income grouped by revenue account. * + * TODO this chart is not multi-currency aware. + * * @param Collection $accounts * @param Collection $categories * @param Carbon $start @@ -119,6 +123,8 @@ class CategoryReportController extends Controller /** * Chart for expenses grouped by expense account. * + * TODO this chart is not multi-currency aware. + * * @param Collection $accounts * @param Collection $categories * @param Carbon $start @@ -148,6 +154,8 @@ class CategoryReportController extends Controller /** * Piechart for income grouped by account. * + * TODO this chart is not multi-currency aware. + * * @param Collection $accounts * @param Collection $categories * @param Carbon $start @@ -177,6 +185,8 @@ class CategoryReportController extends Controller /** * Main report category chart. * + * TODO this chart is not multi-currency aware. + * * @param Collection $accounts * @param Collection $categories * @param Carbon $start diff --git a/app/Http/Controllers/Chart/ExpenseReportController.php b/app/Http/Controllers/Chart/ExpenseReportController.php index 9103a9b64c..baa6ededed 100644 --- a/app/Http/Controllers/Chart/ExpenseReportController.php +++ b/app/Http/Controllers/Chart/ExpenseReportController.php @@ -67,6 +67,8 @@ class ExpenseReportController extends Controller /** * Main chart that shows income and expense for a combination of expense/revenue accounts. * + * TODO this chart is not multi-currency aware. + * * @param Collection $accounts * @param Collection $expense * @param Carbon $start diff --git a/app/Http/Controllers/Chart/PiggyBankController.php b/app/Http/Controllers/Chart/PiggyBankController.php index a22122839b..b67a057b4c 100644 --- a/app/Http/Controllers/Chart/PiggyBankController.php +++ b/app/Http/Controllers/Chart/PiggyBankController.php @@ -55,6 +55,8 @@ class PiggyBankController extends Controller /** * Shows the piggy bank history. * + * TODO this chart is not multi-currency aware. + * * @param PiggyBankRepositoryInterface $repository * @param PiggyBank $piggyBank * diff --git a/app/Http/Controllers/Chart/ReportController.php b/app/Http/Controllers/Chart/ReportController.php index 8a85e95605..309eb36ba1 100644 --- a/app/Http/Controllers/Chart/ReportController.php +++ b/app/Http/Controllers/Chart/ReportController.php @@ -56,6 +56,8 @@ class ReportController extends Controller * This chart, by default, is shown on the multi-year and year report pages, * which means that giving it a 2 week "period" should be enough granularity. * + * TODO this chart is not multi-currency aware. + * * @param Collection $accounts * @param Carbon $start * @param Carbon $end @@ -111,6 +113,8 @@ class ReportController extends Controller /** * Shows income and expense, debit/credit: operations. * + * TODO this chart is not multi-currency aware. + * * @param Collection $accounts * @param Carbon $start * @param Carbon $end @@ -169,6 +173,8 @@ class ReportController extends Controller /** * Shows sum income and expense, debit/credit: operations. * + * TODO this chart is not multi-currency aware. + * * @param Collection $accounts * @param Carbon $start * @param Carbon $end diff --git a/app/Http/Controllers/Chart/TagReportController.php b/app/Http/Controllers/Chart/TagReportController.php index a69d179c1c..6be03027a3 100644 --- a/app/Http/Controllers/Chart/TagReportController.php +++ b/app/Http/Controllers/Chart/TagReportController.php @@ -56,6 +56,8 @@ class TagReportController extends Controller /** * Generate expenses for tags grouped on account. * + * TODO this chart is not multi-currency aware. + * * @param Collection $accounts * @param Collection $tags * @param Carbon $start @@ -85,6 +87,8 @@ class TagReportController extends Controller /** * Generate income for tag grouped by account. * + * TODO this chart is not multi-currency aware. + * * @param Collection $accounts * @param Collection $tags * @param Carbon $start @@ -114,6 +118,8 @@ class TagReportController extends Controller /** * Generate expense for tag grouped on budget. * + * TODO this chart is not multi-currency aware. + * * @param Collection $accounts * @param Collection $tags * @param Carbon $start @@ -142,6 +148,8 @@ class TagReportController extends Controller /** * Generate expense for tag grouped on category. * + * TODO this chart is not multi-currency aware. + * * @param Collection $accounts * @param Collection $tags * @param Carbon $start @@ -170,6 +178,8 @@ class TagReportController extends Controller /** * Generate main tag overview chart. * + * TODO this chart is not multi-currency aware. + * * @param Collection $accounts * @param Collection $tags * @param Carbon $start @@ -283,6 +293,8 @@ class TagReportController extends Controller /** * Show expense grouped by expense account. * + * TODO this chart is not multi-currency aware. + * * @param Collection $accounts * @param Collection $tags * @param Carbon $start @@ -312,6 +324,8 @@ class TagReportController extends Controller /** * Show income grouped by tag. * + * TODO this chart is not multi-currency aware. + * * @param Collection $accounts * @param Collection $tags * @param Carbon $start diff --git a/app/Http/Middleware/SecureHeaders.php b/app/Http/Middleware/SecureHeaders.php index 7616a5119c..f958484a3b 100644 --- a/app/Http/Middleware/SecureHeaders.php +++ b/app/Http/Middleware/SecureHeaders.php @@ -63,15 +63,15 @@ class SecureHeaders $featurePolicies = [ "geolocation 'none'", "midi 'none'", - "notifications 'none'", - "push 'self'", + //"notifications 'none'", + //"push 'self'", "sync-xhr 'self'", "microphone 'none'", "camera 'none'", "magnetometer 'none'", "gyroscope 'none'", "speaker 'none'", - "vibrate 'none'", + //"vibrate 'none'", "fullscreen 'self'", "payment 'none'", ]; diff --git a/app/Models/Transaction.php b/app/Models/Transaction.php index bc52be8698..98d8915bff 100644 --- a/app/Models/Transaction.php +++ b/app/Models/Transaction.php @@ -69,6 +69,7 @@ use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; * @property string $bill_name_encrypted * @property string $notes * @property string $tags + * @property string $transaction_currency_name * @property string $transaction_currency_symbol * @property int $transaction_currency_dp * @property string $transaction_currency_code diff --git a/app/Repositories/Category/CategoryRepository.php b/app/Repositories/Category/CategoryRepository.php index a48c25bef7..2a78a7b8ca 100644 --- a/app/Repositories/Category/CategoryRepository.php +++ b/app/Repositories/Category/CategoryRepository.php @@ -409,6 +409,89 @@ class CategoryRepository implements CategoryRepositoryInterface return (string)$set->sum('transaction_amount'); } + /** @noinspection MoreThanThreeArgumentsInspection */ + + /** + * A very cryptic method name that means: + * + * Get me the amount spent in this period, grouped per currency, where no category was set. + * + * @param Collection $accounts + * @param Carbon $start + * @param Carbon $end + * + * @return array + */ + public function spentInPeriodPcWoCategory(Collection $accounts, Carbon $start, Carbon $end): array + { + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); + $collector->setUser($this->user); + $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->withoutCategory(); + + if ($accounts->count() > 0) { + $collector->setAccounts($accounts); + } + if (0 === $accounts->count()) { + $collector->setAllAssetAccounts(); + } + + $set = $collector->getTransactions(); + $set = $set->filter( + function (Transaction $transaction) { + if (bccomp($transaction->transaction_amount, '0') === -1) { + return $transaction; + } + + return null; + } + ); + + $return = []; + /** @var Transaction $transaction */ + foreach ($set as $transaction) { + $currencyId = $transaction->transaction_currency_id; + $return[$currencyId] = $return[$currencyId] ?? '0'; + $return[$currencyId] = bcadd($return[$currencyId], $transaction->transaction_amount); + } + + return $return; + } + + /** + * @param Collection $categories + * @param Collection $accounts + * @param Carbon $start + * @param Carbon $end + * + * @return array + */ + public function spentInPeriodPerCurrency(Collection $categories, Collection $accounts, Carbon $start, Carbon $end): array + { + /** @var TransactionCollectorInterface $collector */ + $collector = app(TransactionCollectorInterface::class); + $collector->setUser($this->user); + $collector->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setCategories($categories); + + if ($accounts->count() > 0) { + $collector->setAccounts($accounts); + } + if (0 === $accounts->count()) { + $collector->setAllAssetAccounts(); + } + + $set = $collector->getTransactions(); + $return = []; + /** @var Transaction $transaction */ + foreach ($set as $transaction) { + $currencyId = $transaction->transaction_currency_id; + $return[$currencyId] = $return[$currencyId] ?? '0'; + $return[$currencyId] = bcadd($return[$currencyId], $transaction->transaction_amount); + } + + return $return; + } + /** * @param Collection $accounts * @param Carbon $start diff --git a/app/Repositories/Category/CategoryRepositoryInterface.php b/app/Repositories/Category/CategoryRepositoryInterface.php index 0ec74ea07a..5431df23c7 100644 --- a/app/Repositories/Category/CategoryRepositoryInterface.php +++ b/app/Repositories/Category/CategoryRepositoryInterface.php @@ -158,6 +158,31 @@ interface CategoryRepositoryInterface */ public function spentInPeriod(Collection $categories, Collection $accounts, Carbon $start, Carbon $end): string; + /** @noinspection MoreThanThreeArgumentsInspection */ + + /** + * A very cryptic method name that means: + * + * Get me the amount spent in this period, grouped per currency, where no category was set. + * + * @param Collection $accounts + * @param Carbon $start + * @param Carbon $end + * + * @return string + */ + public function spentInPeriodPcWoCategory(Collection $accounts, Carbon $start, Carbon $end): array; + + /** + * @param Collection $categories + * @param Collection $accounts + * @param Carbon $start + * @param Carbon $end + * + * @return array + */ + public function spentInPeriodPerCurrency(Collection $categories, Collection $accounts, Carbon $start, Carbon $end): array; + /** * @param Collection $accounts * @param Carbon $start diff --git a/app/Support/Http/Controllers/AugumentData.php b/app/Support/Http/Controllers/AugumentData.php index e70a25b0a0..bd014b77c1 100644 --- a/app/Support/Http/Controllers/AugumentData.php +++ b/app/Support/Http/Controllers/AugumentData.php @@ -163,7 +163,7 @@ trait AugumentData $return[$categoryId] = $grouped[$categoryId][0]['name']; } } - $return[0] = (string)trans('firefly.noCategory'); + $return[0] = (string)trans('firefly.no_category'); return $return; } diff --git a/public/js/ff/accounts/show.js b/public/js/ff/accounts/show.js index c91cd4f37f..3a3b75e432 100644 --- a/public/js/ff/accounts/show.js +++ b/public/js/ff/accounts/show.js @@ -35,9 +35,9 @@ $(function () { "use strict"; lineChart(chartUri, 'overview-chart'); if (!showAll) { - pieChart(incomeCategoryUri, 'account-cat-in'); - pieChart(expenseCategoryUri, 'account-cat-out'); - pieChart(expenseBudgetUri, 'account-budget-out'); + neutralPieChart(incomeCategoryUri, 'account-cat-in'); + neutralPieChart(expenseCategoryUri, 'account-cat-out'); + neutralPieChart(expenseBudgetUri, 'account-budget-out'); } // sortable! diff --git a/public/js/ff/charts.defaults.js b/public/js/ff/charts.defaults.js index ad00dd96fa..d299625005 100644 --- a/public/js/ff/charts.defaults.js +++ b/public/js/ff/charts.defaults.js @@ -129,4 +129,18 @@ var defaultPieOptions = { }, maintainAspectRatio: true, responsive: true +}; + +var neutralDefaultPieOptions = { + tooltips: { + callbacks: { + label: function (tooltipItem, data) { + "use strict"; + var value = data.datasets[0].data[tooltipItem.index]; + return data.labels[tooltipItem.index] + ': ' + accounting.formatMoney(value, '¤'); + } + } + }, + maintainAspectRatio: true, + responsive: true }; \ No newline at end of file diff --git a/public/js/ff/charts.js b/public/js/ff/charts.js index 301b9a3d6d..e420ac849f 100644 --- a/public/js/ff/charts.js +++ b/public/js/ff/charts.js @@ -204,6 +204,8 @@ function columnChart(URI, container) { drawAChart(URI, container, chartType, options, colorData); } + + /** * * @param URI @@ -255,6 +257,22 @@ function pieChart(URI, container) { } +/** + * + * @param URI + * @param container + */ +function neutralPieChart(URI, container) { + "use strict"; + + var colorData = false; + var options = $.extend(true, {}, neutralDefaultPieOptions); + var chartType = 'pie'; + + drawAChart(URI, container, chartType, options, colorData); + +} + /** * @param URI diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index ddf829a604..054b2bba07 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -183,7 +183,7 @@ return [ 'scopes_will_be_able' => 'This application will be able to:', 'button_authorize' => 'Authorize', 'none_in_select_list' => '(none)', - 'account_in_currency' => ':account in :currency', + 'name_in_currency' => ':name in :currency', // check for updates: 'update_check_title' => 'Check for updates', @@ -811,7 +811,7 @@ return [ 'no_bulk_tags' => 'Don\'t update tag(s)', 'bulk_edit' => 'Bulk edit', 'cannot_edit_other_fields' => 'You cannot mass-edit other fields than the ones here, because there is no room to show them. Please follow the link and edit them by one-by-one, if you need to edit these fields.', - 'no_budget' => 'none', + 'no_budget' => '(no budget)', 'no_budget_squared' => '(no budget)', 'perm-delete-many' => 'Deleting many items in one go can be very disruptive. Please be cautious.', 'mass_deleted_transactions_success' => 'Deleted :amount transaction(s).', diff --git a/tests/Feature/Controllers/Chart/AccountControllerTest.php b/tests/Feature/Controllers/Chart/AccountControllerTest.php index 45e98862d5..ed82b058a0 100644 --- a/tests/Feature/Controllers/Chart/AccountControllerTest.php +++ b/tests/Feature/Controllers/Chart/AccountControllerTest.php @@ -69,8 +69,8 @@ class AccountControllerTest extends TestCase $currencyRepos = $this->mock(CurrencyRepositoryInterface::class); $accountRepos->shouldReceive('getAccountsByType')->withArgs([[AccountType::EXPENSE, AccountType::BENEFICIARY]])->andReturn(new Collection([$account])); - $generator->shouldReceive('singleSet')->andReturn([]); - Steam::shouldReceive('balancesByAccounts')->twice()->andReturn([]); + $generator->shouldReceive('multiSet')->andReturn([]); + Steam::shouldReceive('balancesPerCurrenciesByAccounts')->twice()->andReturn([]); $this->be($this->user()); $this->changeDateRange($this->user(), $range);