From 220d689f69e4372b3cdcaf38c0fce7a22f168253 Mon Sep 17 00:00:00 2001 From: James Cole Date: Mon, 23 Feb 2015 20:25:48 +0100 Subject: [PATCH] Reports! --- app/Helpers/Report/ReportHelper.php | 176 ++++++++ app/Helpers/Report/ReportHelperInterface.php | 62 +++ app/Helpers/Report/ReportQuery.php | 389 ++++++++++++++++++ app/Helpers/Report/ReportQueryInterface.php | 106 +++++ .../Controllers/GoogleChartController.php | 150 +++++++ app/Http/Controllers/ReportController.php | 206 ++++++++++ app/Http/routes.php | 12 +- app/Providers/FireflyServiceProvider.php | 4 +- app/Support/Steam.php | 130 ++++++ database/seeds/TestDataSeeder.php | 16 +- public/js/reports.js | 9 + resources/views/reports/budget.blade.php | 156 +++++++ resources/views/reports/index.blade.php | 50 +++ resources/views/reports/month.blade.php | 235 +++++++++++ resources/views/reports/year.blade.php | 170 ++++++++ 15 files changed, 1861 insertions(+), 10 deletions(-) create mode 100644 app/Helpers/Report/ReportHelper.php create mode 100644 app/Helpers/Report/ReportHelperInterface.php create mode 100644 app/Helpers/Report/ReportQuery.php create mode 100644 app/Helpers/Report/ReportQueryInterface.php create mode 100644 app/Http/Controllers/ReportController.php create mode 100644 public/js/reports.js create mode 100644 resources/views/reports/budget.blade.php create mode 100644 resources/views/reports/index.blade.php create mode 100644 resources/views/reports/month.blade.php create mode 100644 resources/views/reports/year.blade.php diff --git a/app/Helpers/Report/ReportHelper.php b/app/Helpers/Report/ReportHelper.php new file mode 100644 index 0000000000..3d709dcc2b --- /dev/null +++ b/app/Helpers/Report/ReportHelper.php @@ -0,0 +1,176 @@ +_queries->journalsByExpenseAccount($start, $end); + $array = $this->_helper->makeArray($result); + $limited = $this->_helper->limitArray($array, $limit); + + return $limited; + + } + + /** + * @return Carbon + */ + public function firstDate() + { + $journal = Auth::user()->transactionjournals()->orderBy('date', 'ASC')->first(); + if ($journal) { + return $journal->date; + } + + return Carbon::now(); + } + + /** + * This method gets some kind of list for a monthly overview. + * + * @param Carbon $date + * + * @return Collection + */ + public function getBudgetsForMonth(Carbon $date) + { + $start = clone $date; + $start->startOfMonth(); + $end = clone $date; + $end->endOfMonth(); + // all budgets + $set = \Auth::user()->budgets() + ->leftJoin( + 'budget_limits', function (JoinClause $join) use ($date) { + $join->on('budget_limits.budget_id', '=', 'budgets.id')->where('budget_limits.startdate', '=', $date->format('Y-m-d')); + } + ) + ->get(['budgets.*', 'budget_limits.amount as amount']); + + + + $budgets = $this->_helper->makeArray($set); + $amountSet = $this->_queries->journalsByBudget($start, $end); + $amounts = $this->_helper->makeArray($amountSet); + $combined = $this->_helper->mergeArrays($budgets, $amounts); + $combined[0]['spent'] = isset($combined[0]['spent']) ? $combined[0]['spent'] : 0.0; + $combined[0]['amount'] = isset($combined[0]['amount']) ? $combined[0]['amount'] : 0.0; + $combined[0]['name'] = 'No budget'; + + // find transactions to shared expense accounts, which are without a budget by default: + $transfers = $this->_queries->sharedExpenses($start, $end); + foreach ($transfers as $transfer) { + $combined[0]['spent'] += floatval($transfer->amount) * -1; + } + + return $combined; + } + + /** + * @param Carbon $date + * + * @return array + */ + public function listOfMonths(Carbon $date) + { + $start = clone $date; + $end = Carbon::now(); + $months = []; + while ($start <= $end) { + $months[] = [ + 'formatted' => $start->format('F Y'), + 'month' => intval($start->format('m')), + 'year' => intval($start->format('Y')), + ]; + $start->addMonth(); + } + + return $months; + } + + /** + * @param Carbon $date + * + * @return array + */ + public function listOfYears(Carbon $date) + { + $start = clone $date; + $end = Carbon::now(); + $years = []; + while ($start <= $end) { + $years[] = $start->format('Y'); + $start->addYear(); + } + + return $years; + } + + /** + * @param Carbon $date + * + * @return array + */ + public function yearBalanceReport(Carbon $date) + { + $start = clone $date; + $end = clone $date; + $sharedAccounts = []; + $sharedCollection = \Auth::user()->accounts() + ->leftJoin('account_meta', 'account_meta.account_id', '=', 'accounts.id') + ->where('account_meta.name', '=', 'accountRole') + ->where('account_meta.data', '=', json_encode('sharedExpense')) + ->get(['accounts.id']); + + foreach ($sharedCollection as $account) { + $sharedAccounts[] = $account->id; + } + + $accounts = \Auth::user()->accounts()->accountTypeIn(['Default account', 'Asset account'])->get()->filter( + function (Account $account) use ($sharedAccounts) { + if (!in_array($account->id, $sharedAccounts)) { + return $account; + } + + return null; + } + ); + $report = []; + $start->startOfYear()->subDay(); + $end->endOfYear(); + + foreach ($accounts as $account) { + $report[] = [ + 'start' => \Steam::balance($account, $start), + 'end' => \Steam::balance($account, $end), + 'account' => $account, + 'shared' => $account->accountRole == 'sharedExpense' + ]; + } + + return $report; + } +} \ No newline at end of file diff --git a/app/Helpers/Report/ReportHelperInterface.php b/app/Helpers/Report/ReportHelperInterface.php new file mode 100644 index 0000000000..2284523ff8 --- /dev/null +++ b/app/Helpers/Report/ReportHelperInterface.php @@ -0,0 +1,62 @@ +accounts() + ->leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id') + ->leftJoin( + 'account_meta', function (JoinClause $join) { + $join->on('account_meta.account_id', '=', 'accounts.id')->where('account_meta.name', '=', "accountRole"); + } + ) + ->whereIn('account_types.type', ['Default account', 'Cash account', 'Asset account']) + ->where('active', 1) + ->where( + function (Builder $query) { + $query->where('account_meta.data', '!=', '"sharedExpense"'); + $query->orWhereNull('account_meta.data'); + } + ) + ->get(['accounts.*']); + } + + /** + * This method returns all "income" journals in a certain period, which are both transfers from a shared account + * and "ordinary" deposits. The query used is almost equal to ReportQueryInterface::journalsByRevenueAccount but it does + * not group and returns different fields. + * + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + public function incomeByPeriod(Carbon $start, Carbon $end) + { + return TransactionJournal:: + leftJoin( + 'transactions as t_from', function (JoinClause $join) { + $join->on('t_from.transaction_journal_id', '=', 'transaction_journals.id')->where('t_from.amount', '<', 0); + } + ) + ->leftJoin('accounts as ac_from', 't_from.account_id', '=', 'ac_from.id') + ->leftJoin( + 'account_meta as acm_from', function (JoinClause $join) { + $join->on('ac_from.id', '=', 'acm_from.account_id')->where('acm_from.name', '=', 'accountRole'); + } + ) + ->leftJoin( + 'transactions as t_to', function (JoinClause $join) { + $join->on('t_to.transaction_journal_id', '=', 'transaction_journals.id')->where('t_to.amount', '>', 0); + } + ) + ->leftJoin('accounts as ac_to', 't_to.account_id', '=', 'ac_to.id') + ->leftJoin( + 'account_meta as acm_to', function (JoinClause $join) { + $join->on('ac_to.id', '=', 'acm_to.account_id')->where('acm_to.name', '=', 'accountRole'); + } + ) + ->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') + ->where( + function ($query) { + $query->where( + function ($q) { + $q->where('transaction_types.type', 'Deposit'); + $q->where('acm_to.data', '!=', '"sharedExpense"'); + } + ); + $query->orWhere( + function ($q) { + $q->where('transaction_types.type', 'Transfer'); + $q->where('acm_from.data', '=', '"sharedExpense"'); + } + ); + } + ) + ->before($end)->after($start) + ->where('transaction_journals.user_id', Auth::user()->id) + ->groupBy('t_from.account_id')->orderBy('transaction_journals.date') + ->get( + ['transaction_journals.id', + 'transaction_journals.description', + 'transaction_journals.encrypted', + 'transaction_types.type', + 't_to.amount', 'transaction_journals.date', 't_from.account_id as account_id', + 'ac_from.name as name'] + ); + } + + /** + * Gets a list of expenses grouped by the budget they were filed under. + * + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + public function journalsByBudget(Carbon $start, Carbon $end) + { + return \Auth::user()->transactionjournals() + ->leftJoin('budget_transaction_journal', 'budget_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id') + ->leftJoin('budgets', 'budget_transaction_journal.budget_id', '=', 'budgets.id') + ->leftJoin( + 'transactions', function (JoinClause $join) { + $join->on('transaction_journals.id', '=', 'transactions.transaction_journal_id')->where('transactions.amount', '<', 0); + } + ) + ->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id') + ->leftJoin( + 'account_meta', function (JoinClause $join) { + $join->on('account_meta.account_id', '=', 'accounts.id')->where('account_meta.name', '=', 'accountRole'); + } + ) + ->leftJoin('transaction_types', 'transaction_journals.transaction_type_id', '=', 'transaction_types.id') + ->where('transaction_journals.date', '>=', $start->format('Y-m-d')) + ->where('transaction_journals.date', '<=', $end->format('Y-m-d')) + ->where('account_meta.data', '!=', '"sharedExpense"') + ->where('transaction_types.type', 'Withdrawal') + ->groupBy('budgets.id') + ->orderBy('budgets.name', 'ASC') + ->get(['budgets.id', 'budgets.name', \DB::Raw('SUM(`transactions`.`amount`) AS `spent`')]); + } + + /** + * Gets a list of categories and the expenses therein, grouped by the relevant category. + * This result excludes transfers to shared accounts which are expenses, technically. + * + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + public function journalsByCategory(Carbon $start, Carbon $end) + { + return \Auth::user()->transactionjournals() + ->leftJoin( + 'category_transaction_journal', 'category_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id' + ) + ->leftJoin('categories', 'category_transaction_journal.category_id', '=', 'categories.id') + ->leftJoin( + 'transactions', function (JoinClause $join) { + $join->on('transaction_journals.id', '=', 'transactions.transaction_journal_id')->where('transactions.amount', '<', 0); + } + ) + ->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id') + ->leftJoin( + 'account_meta', function (JoinClause $join) { + $join->on('account_meta.account_id', '=', 'accounts.id')->where('account_meta.name', '=', 'accountRole'); + } + ) + ->leftJoin('transaction_types', 'transaction_journals.transaction_type_id', '=', 'transaction_types.id') + ->where('transaction_journals.date', '>=', $start->format('Y-m-d')) + ->where('transaction_journals.date', '<=', $end->format('Y-m-d')) + ->where('account_meta.data', '!=', '"sharedExpense"') + ->where('transaction_types.type', 'Withdrawal') + ->groupBy('categories.id') + ->orderBy('amount') + ->get(['categories.id', 'categories.name', \DB::Raw('SUM(`transactions`.`amount`) AS `amount`')]); + + } + + /** + * Gets a list of expense accounts and the expenses therein, grouped by that expense account. + * This result excludes transfers to shared accounts which are expenses, technically. + * + * So now it will include them! + * + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + public function journalsByExpenseAccount(Carbon $start, Carbon $end) + { + return TransactionJournal:: + leftJoin( + 'transactions as t_from', function (JoinClause $join) { + $join->on('t_from.transaction_journal_id', '=', 'transaction_journals.id')->where('t_from.amount', '<', 0); + } + ) + ->leftJoin('accounts as ac_from', 't_from.account_id', '=', 'ac_from.id') + ->leftJoin( + 'account_meta as acm_from', function (JoinClause $join) { + $join->on('ac_from.id', '=', 'acm_from.account_id')->where('acm_from.name', '=', 'accountRole'); + } + ) + ->leftJoin( + 'transactions as t_to', function (JoinClause $join) { + $join->on('t_to.transaction_journal_id', '=', 'transaction_journals.id')->where('t_to.amount', '>', 0); + } + ) + ->leftJoin('accounts as ac_to', 't_to.account_id', '=', 'ac_to.id') + ->leftJoin( + 'account_meta as acm_to', function (JoinClause $join) { + $join->on('ac_to.id', '=', 'acm_to.account_id')->where('acm_to.name', '=', 'accountRole'); + } + ) + ->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') + ->where( + function ($query) { + $query->where( + function ($q) { + $q->where('transaction_types.type', 'Withdrawal'); + $q->where('acm_from.data', '!=', '"sharedExpense"'); + } + ); + $query->orWhere( + function ($q) { + $q->where('transaction_types.type', 'Transfer'); + $q->where('acm_to.data', '=', '"sharedExpense"'); + } + ); + } + ) + ->before($end) + ->after($start) + ->where('transaction_journals.user_id', Auth::user()->id) + ->groupBy('t_to.account_id') + ->orderBy('amount', 'DESC') + ->get(['t_to.account_id as id', 'ac_to.name as name', DB::Raw('SUM(t_to.amount) as `amount`')]); + } + + /** + * This method returns all deposits into asset accounts, grouped by the revenue account, + * + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + public function journalsByRevenueAccount(Carbon $start, Carbon $end) + { + return TransactionJournal:: + leftJoin( + 'transactions as t_from', function (JoinClause $join) { + $join->on('t_from.transaction_journal_id', '=', 'transaction_journals.id')->where('t_from.amount', '<', 0); + } + ) + ->leftJoin('accounts as ac_from', 't_from.account_id', '=', 'ac_from.id') + ->leftJoin( + 'account_meta as acm_from', function (JoinClause $join) { + $join->on('ac_from.id', '=', 'acm_from.account_id')->where('acm_from.name', '=', 'accountRole'); + } + ) + ->leftJoin( + 'transactions as t_to', function (JoinClause $join) { + $join->on('t_to.transaction_journal_id', '=', 'transaction_journals.id')->where('t_to.amount', '>', 0); + } + ) + ->leftJoin('accounts as ac_to', 't_to.account_id', '=', 'ac_to.id') + ->leftJoin( + 'account_meta as acm_to', function (JoinClause $join) { + $join->on('ac_to.id', '=', 'acm_to.account_id')->where('acm_to.name', '=', 'accountRole'); + } + ) + ->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id') + ->where( + function ($query) { + $query->where( + function ($q) { + $q->where('transaction_types.type', 'Deposit'); + $q->where('acm_to.data', '!=', '"sharedExpense"'); + } + ); + $query->orWhere( + function ($q) { + $q->where('transaction_types.type', 'Transfer'); + $q->where('acm_from.data', '=', '"sharedExpense"'); + } + ); + } + ) + ->before($end)->after($start) + ->where('transaction_journals.user_id', Auth::user()->id) + ->groupBy('t_from.account_id')->orderBy('amount') + ->get(['t_from.account_id as account_id', 'ac_from.name as name', DB::Raw('SUM(t_from.amount) as `amount`')]); + } + + /** + * With an equally misleading name, this query returns are transfers to shared accounts. These are considered + * expenses. + * + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + public function sharedExpenses(Carbon $start, Carbon $end) + { + return TransactionJournal:: + 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 + ); + } + ) + ->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id') + ->leftJoin( + 'account_meta', function (JoinClause $join) { + $join->on('account_meta.account_id', '=', 'accounts.id')->where('account_meta.name', '=', 'accountRole'); + } + ) + ->where('account_meta.data', '"sharedExpense"') + ->after($start) + ->before($end) + ->where('transaction_types.type', 'Transfer') + ->where('transaction_journals.user_id', \Auth::user()->id) + ->get( + ['transaction_journals.id', 'transaction_journals.description', 'transactions.account_id', 'accounts.name', + 'transactions.amount'] + ); + + } + + /** + * With a slightly misleading name, this query returns all transfers to shared accounts + * which are technically expenses, since it won't be just your money that gets spend. + * + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + public function sharedExpensesByCategory(Carbon $start, Carbon $end) + { + return TransactionJournal:: + 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 + ); + } + ) + ->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id') + ->leftJoin( + 'account_meta', function (JoinClause $join) { + $join->on('account_meta.account_id', '=', 'accounts.id')->where('account_meta.name', '=', 'accountRole'); + } + ) + ->leftJoin( + 'category_transaction_journal', 'category_transaction_journal.transaction_journal_id', '=', 'transaction_journals.id' + ) + ->leftJoin('categories', 'category_transaction_journal.category_id', '=', 'categories.id') + ->where('account_meta.data', '"sharedExpense"') + ->after($start) + ->before($end) + ->where('transaction_types.type', 'Transfer') + ->where('transaction_journals.user_id', \Auth::user()->id) + ->groupBy('categories.name') + ->get( + [ + 'categories.id', + 'categories.name as name', + \DB::Raw('SUM(`transactions`.`amount`) * -1 AS `amount`') + ] + ); + } + +} \ No newline at end of file diff --git a/app/Helpers/Report/ReportQueryInterface.php b/app/Helpers/Report/ReportQueryInterface.php new file mode 100644 index 0000000000..3d90390fe8 --- /dev/null +++ b/app/Helpers/Report/ReportQueryInterface.php @@ -0,0 +1,106 @@ +with('message', 'Invalid year.'); + } + $chart->addColumn('Month', 'date'); + $chart->addColumn('Income', 'number'); + $chart->addColumn('Expenses', 'number'); + + // get report query interface. + + $end = clone $start; + $end->endOfYear(); + while ($start < $end) { + $currentEnd = clone $start; + $currentEnd->endOfMonth(); + // total income: + $income = $query->incomeByPeriod($start, $currentEnd); + $incomeSum = 0; + foreach ($income as $entry) { + $incomeSum += floatval($entry->amount); + } + + // total expenses: + $expense = $query->journalsByExpenseAccount($start, $currentEnd); + $expenseSum = 0; + foreach ($expense as $entry) { + $expenseSum += floatval($entry->amount); + } + + $chart->addRow(clone $start, $incomeSum, $expenseSum); + $start->addMonth(); + } + + + $chart->generate(); + + return Response::json($chart->getData()); + + } + + /** + * + * @param $year + * + * @return \Illuminate\Http\JsonResponse + */ + public function yearInExpSum($year, GChart $chart, ReportQueryInterface $query) + { + try { + $start = new Carbon('01-01-' . $year); + } catch (Exception $e) { + return view('error')->with('message', 'Invalid year.'); + } + $chart->addColumn('Summary', 'string'); + $chart->addColumn('Income', 'number'); + $chart->addColumn('Expenses', 'number'); + + $income = 0; + $expense = 0; + $count = 0; + + $end = clone $start; + $end->endOfYear(); + while ($start < $end) { + $currentEnd = clone $start; + $currentEnd->endOfMonth(); + // total income: + $incomeResult = $query->incomeByPeriod($start, $currentEnd); + $incomeSum = 0; + foreach ($incomeResult as $entry) { + $incomeSum += floatval($entry->amount); + } + + // total expenses: + $expenseResult = $query->journalsByExpenseAccount($start, $currentEnd); + $expenseSum = 0; + foreach ($expenseResult as $entry) { + $expenseSum += floatval($entry->amount); + } + + $income += $incomeSum; + $expense += $expenseSum; + $count++; + $start->addMonth(); + } + + + $chart->addRow('Sum', $income, $expense); + $count = $count > 0 ? $count : 1; + $chart->addRow('Average', ($income / $count), ($expense / $count)); + + $chart->generate(); + + return Response::json($chart->getData()); + + } + + + /** + * @param int $year + * + * @return $this|\Illuminate\Http\JsonResponse + */ + public function allBudgetsAndSpending($year, GChart $chart, BudgetRepositoryInterface $repository) + { + try { + new Carbon('01-01-' . $year); + } catch (Exception $e) { + return view('error')->with('message', 'Invalid year.'); + } + $budgets = Auth::user()->budgets()->get(); + $budgets->sortBy('name'); + $chart->addColumn('Month', 'date'); + foreach ($budgets as $budget) { + $chart->addColumn($budget->name, 'number'); + } + $start = Carbon::createFromDate(intval($year), 1, 1); + $end = clone $start; + $end->endOfYear(); + + + while ($start <= $end) { + $row = [clone $start]; + foreach ($budgets as $budget) { + $spent = $repository->spentInMonth($budget, $start); + $row[] = $spent; + } + $chart->addRowArray($row); + $start->addMonth(); + } + + + $chart->generate(); + + return Response::json($chart->getData()); + + } + } diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php new file mode 100644 index 0000000000..f0adba9b4c --- /dev/null +++ b/app/Http/Controllers/ReportController.php @@ -0,0 +1,206 @@ +firstDate(); + $months = $helper->listOfMonths($start); + $years = $helper->listOfYears($start); + $title = 'Reports'; + $mainTitleIcon = 'fa-line-chart'; + + return view('reports.index', compact('years', 'months', 'title', 'mainTitleIcon')); + } + + /** + * @param string $year + * @param string $month + * + * @return \Illuminate\View\View + */ + public function month($year = '2014', $month = '1', ReportQueryInterface $query) + { + try { + new Carbon($year . '-' . $month . '-01'); + } catch (Exception $e) { + return View::make('error')->with('message', 'Invalid date.'); + } + $date = new Carbon($year . '-' . $month . '-01'); + $subTitle = 'Report for ' . $date->format('F Y'); + $subTitleIcon = 'fa-calendar'; + $displaySum = true; // to show sums in report. + + + /** + * + * get income for month (date) + * + */ + + $start = clone $date; + $start->startOfMonth(); + $end = clone $date; + $end->endOfMonth(); + + /** + * Start getIncomeForMonth DONE + */ + $income = $query->incomeByPeriod($start, $end); + /** + * End getIncomeForMonth DONE + */ + /** + * Start getExpenseGroupedForMonth DONE + */ + $set = $query->journalsByExpenseAccount($start, $end); + $expenses = Steam::makeArray($set); + $expenses = Steam::sortArray($expenses); + $expenses = Steam::limitArray($expenses, 10); + /** + * End getExpenseGroupedForMonth DONE + */ + /** + * Start getBudgetsForMonth DONE + */ + $set = Auth::user()->budgets() + ->leftJoin( + 'budget_limits', function (JoinClause $join) use ($date) { + $join->on('budget_limits.budget_id', '=', 'budgets.id')->where('budget_limits.startdate', '=', $date->format('Y-m-d')); + } + ) + ->get(['budgets.*', 'budget_limits.amount as amount']); + $budgets = Steam::makeArray($set); + $amountSet = $query->journalsByBudget($start, $end); + $amounts = Steam::makeArray($amountSet); + $budgets = Steam::mergeArrays($budgets, $amounts); + $budgets[0]['spent'] = isset($budgets[0]['spent']) ? $budgets[0]['spent'] : 0.0; + $budgets[0]['amount'] = isset($budgets[0]['amount']) ? $budgets[0]['amount'] : 0.0; + $budgets[0]['name'] = 'No budget'; + + // find transactions to shared expense accounts, which are without a budget by default: + $transfers = $query->sharedExpenses($start, $end); + foreach ($transfers as $transfer) { + $budgets[0]['spent'] += floatval($transfer->amount) * -1; + } + + /** + * End getBudgetsForMonth DONE + */ + /** + * Start getCategoriesForMonth DONE + */ + // all categories. + $result = $query->journalsByCategory($start, $end); + $categories = Steam::makeArray($result); + + // all transfers + $result = $query->sharedExpensesByCategory($start, $end); + $transfers = Steam::makeArray($result); + $merged = Steam::mergeArrays($categories, $transfers); + + // sort. + $sorted = Steam::sortNegativeArray($merged); + + // limit to $limit: + $categories = Steam::limitArray($sorted, 10); + /** + * End getCategoriesForMonth DONE + */ + /** + * Start getAccountsForMonth + */ + $list = $query->accountList(); + $accounts = []; + /** @var Account $account */ + foreach ($list as $account) { + $id = intval($account->id); + /** @noinspection PhpParamsInspection */ + $accounts[$id] = [ + 'name' => $account->name, + 'startBalance' => Steam::balance($account, $start), + 'endBalance' => Steam::balance($account, $end) + ]; + + $accounts[$id]['difference'] = $accounts[$id]['endBalance'] - $accounts[$id]['startBalance']; + } + + /** + * End getAccountsForMonth + */ + + + return View::make( + 'reports.month', + compact( + 'income', 'expenses', 'budgets', 'accounts', 'categories', + 'date', 'subTitle', 'displaySum', 'subTitleIcon' + ) + ); + } + + /** + * @param $year + * + * @return $this + */ + public function year($year, ReportHelperInterface $helper, ReportQueryInterface $query) + { + try { + new Carbon('01-01-' . $year); + } catch (Exception $e) { + return View::make('error')->with('message', 'Invalid date.'); + } + $date = new Carbon('01-01-' . $year); + $end = clone $date; + $end->endOfYear(); + $title = 'Reports'; + $subTitle = $year; + $subTitleIcon = 'fa-bar-chart'; + $mainTitleIcon = 'fa-line-chart'; + $balances = $helper->yearBalanceReport($date); + $groupedIncomes = $query->journalsByRevenueAccount($date, $end); + $groupedExpenses = $query->journalsByExpenseAccount($date, $end); + + //$groupedExpenses = $helper-> expensesGroupedByAccount($date, $end, 15); + + return View::make( + 'reports.year', compact('date', 'groupedIncomes', 'groupedExpenses', 'year', 'balances', 'title', 'subTitle', 'subTitleIcon', 'mainTitleIcon') + ); + } + + +} diff --git a/app/Http/routes.php b/app/Http/routes.php index 0a6fab87ea..955a9e6867 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -149,9 +149,11 @@ Route::group( Route::get('/chart/account/{account}/{view?}', ['uses' => 'GoogleChartController@accountBalanceChart']); Route::get('/chart/budget/{budget}/spending/{year?}', ['uses' => 'GoogleChartController@budgetsAndSpending']); + Route::get('/chart/budgets/spending/{year?}', ['uses' => 'GoogleChartController@allBudgetsAndSpending']); Route::get('/chart/budget/{budget}/{limitrepetition}', ['uses' => 'GoogleChartController@budgetLimitSpending']); - //Route::get('/chart/reports/income-expenses/{year}', ['uses' => 'GoogleChartController@yearInExp']); - //Route::get('/chart/reports/income-expenses-sum/{year}', ['uses' => 'GoogleChartController@yearInExpSum']); + + Route::get('/chart/reports/income-expenses/{year}', ['uses' => 'GoogleChartController@yearInExp']); + Route::get('/chart/reports/income-expenses-sum/{year}', ['uses' => 'GoogleChartController@yearInExpSum']); //Route::get('/chart/bills/{bill}', ['uses' => 'GoogleChartController@billOverview']); @@ -191,9 +193,9 @@ Route::group( * Report Controller */ Route::get('/reports', ['uses' => 'ReportController@index', 'as' => 'reports.index']); - //Route::get('/reports/{year}', ['uses' => 'ReportController@year', 'as' => 'reports.year']); - //Route::get('/reports/{year}/{month}', ['uses' => 'ReportController@month', 'as' => 'reports.month']); - //Route::get('/reports/budget/{year}/{month}', ['uses' => 'ReportController@budget', 'as' => 'reports.budget']); + Route::get('/reports/{year}', ['uses' => 'ReportController@year', 'as' => 'reports.year']); + Route::get('/reports/{year}/{month}', ['uses' => 'ReportController@month', 'as' => 'reports.month']); + Route::get('/reports/budget/{year}/{month}', ['uses' => 'ReportController@budget', 'as' => 'reports.budget']); /** * Search Controller diff --git a/app/Providers/FireflyServiceProvider.php b/app/Providers/FireflyServiceProvider.php index 26ab9d0ebd..49c7c2907a 100644 --- a/app/Providers/FireflyServiceProvider.php +++ b/app/Providers/FireflyServiceProvider.php @@ -56,12 +56,14 @@ class FireflyServiceProvider extends ServiceProvider } ); - // preferences $this->app->bind('FireflyIII\Repositories\Account\AccountRepositoryInterface', 'FireflyIII\Repositories\Account\AccountRepository'); $this->app->bind('FireflyIII\Repositories\Budget\BudgetRepositoryInterface', 'FireflyIII\Repositories\Budget\BudgetRepository'); $this->app->bind('FireflyIII\Repositories\Category\CategoryRepositoryInterface', 'FireflyIII\Repositories\Category\CategoryRepository'); $this->app->bind('FireflyIII\Repositories\Journal\JournalRepositoryInterface', 'FireflyIII\Repositories\Journal\JournalRepository'); + $this->app->bind('FireflyIII\Helpers\Report\ReportHelperInterface', 'FireflyIII\Helpers\Report\ReportHelper'); + $this->app->bind('FireflyIII\Helpers\Report\ReportQueryInterface', 'FireflyIII\Helpers\Report\ReportQuery'); + } } \ No newline at end of file diff --git a/app/Support/Steam.php b/app/Support/Steam.php index 2f0acb6b61..750ac26777 100644 --- a/app/Support/Steam.php +++ b/app/Support/Steam.php @@ -4,6 +4,7 @@ namespace FireflyIII\Support; use Carbon\Carbon; use FireflyIII\Models\Account; +use Illuminate\Support\Collection; /** * Class Steam @@ -31,4 +32,133 @@ class Steam return $balance; } + /** + * Turns a collection into an array. Needs the field 'id' for the key, + * and saves only 'name' and 'amount' as a sub array. + * + * @param Collection $collection + * + * @return array + */ + public function makeArray(Collection $collection) + { + $array = []; + foreach ($collection as $entry) { + $entry->spent = isset($entry->spent) ? floatval($entry->spent) : 0.0; + $id = intval($entry->id); + if (isset($array[$id])) { + $array[$id]['amount'] += floatval($entry->amount); + $array[$id]['spent'] += floatval($entry->spent); + } else { + $array[$id] = [ + 'amount' => floatval($entry->amount), + 'spent' => floatval($entry->spent), + 'name' => $entry->name + ]; + } + } + + return $array; + } + + /** + * Merges two of the arrays as defined above. Can't handle more (yet) + * + * @param array $one + * @param array $two + * + * @return array + */ + public function mergeArrays(array $one, array $two) + { + foreach ($two as $id => $value) { + // $otherId also exists in $one: + if (isset($one[$id])) { + $one[$id]['amount'] += $value['amount']; + $one[$id]['spent'] += $value['spent']; + } else { + $one[$id] = $value; + } + } + + return $one; + } + + /** + * Sort an array where all 'amount' keys are positive floats. + * + * @param array $array + * + * @return array + */ + public function sortArray(array $array) + { + uasort( + $array, function ($left, $right) { + if ($left['amount'] == $right['amount']) { + return 0; + } + + return ($left['amount'] < $right['amount']) ? 1 : -1; + } + ); + + return $array; + + } + + /** + * Only return the top X entries, group the rest by amount + * and described as 'Others'. id = 0 as well + * + * @param array $array + * @param int $limit + * + * @return array + */ + public function limitArray(array $array, $limit = 10) + { + $others = [ + 'name' => 'Others', + 'amount' => 0 + ]; + $return = []; + $count = 0; + foreach ($array as $id => $entry) { + if ($count < ($limit - 1)) { + $return[$id] = $entry; + } else { + $others['amount'] += $entry['amount']; + } + + $count++; + } + $return[0] = $others; + + return $return; + + } + + /** + * Sort an array where all 'amount' keys are negative floats. + * + * @param array $array + * + * @return array + */ + public function sortNegativeArray(array $array) + { + uasort( + $array, function ($left, $right) { + if ($left['amount'] == $right['amount']) { + return 0; + } + + return ($left['amount'] < $right['amount']) ? -1 : 1; + } + ); + + return $array; + } + } \ No newline at end of file diff --git a/database/seeds/TestDataSeeder.php b/database/seeds/TestDataSeeder.php index 34a6acfd61..b48e34dade 100644 --- a/database/seeds/TestDataSeeder.php +++ b/database/seeds/TestDataSeeder.php @@ -1,12 +1,12 @@ $user->id, 'account_type_id' => $assetType->id, 'name' => 'Savings account', 'active' => 1]); $acc_c = Account::create(['user_id' => $user->id, 'account_type_id' => $assetType->id, 'name' => 'Delete me', 'active' => 1]); + // create account meta: + $meta_a = AccountMeta::create(['account_id' => $acc_a->id, 'name' => 'accountRole', 'data' => 'defaultExpense']); + $meta_b = AccountMeta::create(['account_id' => $acc_b->id, 'name' => 'accountRole', 'data' => 'defaultExpense']); + $meta_c = AccountMeta::create(['account_id' => $acc_c->id, 'name' => 'accountRole', 'data' => 'defaultExpense']); +// var_dump($meta_a->toArray()); +// var_dump($meta_b->toArray()); +// var_dump($meta_c->toArray()); + $acc_d = Account::create(['user_id' => $user->id, 'account_type_id' => $ibType->id, 'name' => 'Checking account initial balance', 'active' => 0]); $acc_e = Account::create(['user_id' => $user->id, 'account_type_id' => $ibType->id, 'name' => 'Savings account initial balance', 'active' => 0]); $acc_f = Account::create(['user_id' => $user->id, 'account_type_id' => $ibType->id, 'name' => 'Delete me initial balance', 'active' => 0]); @@ -215,9 +223,9 @@ class TestDataSeeder extends Seeder ); // and because we have no filters, some repetitions: -// LimitRepetition::create(['budget_limit_id' => $groceriesLimit->id, 'startdate' => $this->som, 'enddate' => $this->eom, 'amount' => 201]); -// LimitRepetition::create(['budget_limit_id' => $billsLimit->id, 'startdate' => $this->som, 'enddate' => $this->eom, 'amount' => 202]); -// LimitRepetition::create(['budget_limit_id' => $deleteMeLimit->id, 'startdate' => $this->som, 'enddate' => $this->eom, 'amount' => 203]); + // LimitRepetition::create(['budget_limit_id' => $groceriesLimit->id, 'startdate' => $this->som, 'enddate' => $this->eom, 'amount' => 201]); + // LimitRepetition::create(['budget_limit_id' => $billsLimit->id, 'startdate' => $this->som, 'enddate' => $this->eom, 'amount' => 202]); + // LimitRepetition::create(['budget_limit_id' => $deleteMeLimit->id, 'startdate' => $this->som, 'enddate' => $this->eom, 'amount' => 203]); } /** diff --git a/public/js/reports.js b/public/js/reports.js new file mode 100644 index 0000000000..4dee6cbbdd --- /dev/null +++ b/public/js/reports.js @@ -0,0 +1,9 @@ +if (typeof(google) != 'undefined') { + google.setOnLoadCallback(drawChart); + function drawChart() { + googleColumnChart('chart/reports/income-expenses/' + year, 'income-expenses-chart'); + googleColumnChart('chart/reports/income-expenses-sum/' + year, 'income-expenses-sum-chart') + + googleStackedColumnChart('chart/budgets/spending/' + year, 'budgets'); + } +} \ No newline at end of file diff --git a/resources/views/reports/budget.blade.php b/resources/views/reports/budget.blade.php new file mode 100644 index 0000000000..bab3a63918 --- /dev/null +++ b/resources/views/reports/budget.blade.php @@ -0,0 +1,156 @@ +@extends('layouts.default') +@section('content') +{{ Breadcrumbs::renderIfExists(Route::getCurrentRoute()->getName(), $date) }} +
+
+ + + + + + + + @foreach($accounts as $account) + + + + + + + @endforeach +
AccountStart of monthCurrent balanceSpent
{{{$account->name}}}{{Amount::format($account->startBalance)}}{{Amount::format($account->endBalance)}}{{Amount::format($account->startBalance - $account->endBalance,false)}}
+
+
+
+
+ + + + + + @foreach($accounts as $account) + + id] = 0; + ?> + @endforeach + + + @foreach($budgets as $id => $budget) + + + + + @foreach($accounts as $account) + @if(isset($account->budgetInformation[$id])) + + budgetInformation[$id]['amount']); + $accountSums[$account->id] += floatval($account->budgetInformation[$id]['amount']); + ?> + @else + + @endif + @endforeach + + + + @endforeach + + + @foreach($accounts as $account) + @if(isset($account->budgetInformation[0])) + + @else + + @endif + @endforeach + + + + + @foreach($accounts as $account) + + @endforeach + + + + + + @foreach($accounts as $account) + id] += $account->balancedAmount; + ?> + @if(isset($account->budgetInformation[0])) + + @else + + @endif + @endforeach + + + + + @foreach($accounts as $account) + + @endforeach + + + + + @foreach($accounts as $account) + + @endforeach + + + +
Budgets{{{$account->name}}} + Left in budget +
{{{$budget['name']}}} + @if($id == 0) + + @endif + {{Amount::format($budget['amount'])}} + @if($id == 0) + {{Amount::format($account->budgetInformation[$id]['amount'])}} + @else + {{Amount::format($account->budgetInformation[$id]['amount'])}} + @endif + {{Amount::format(0)}}{{Amount::format($budget['amount'] + $budget['spent'])}}{{Amount::format($budget['amount'] + $spent)}}
Without budget + + + {{Amount::format($account->budgetInformation[0]['amount'])}} + {{Amount::format(0)}} 
Balanced by transfers + {{Amount::format($account->balancedAmount)}} +  
Left unbalanced + {{Amount::format($account->budgetInformation[0]['amount'] + $account->balancedAmount)}} + {{Amount::format(0)}} 
Sum{{Amount::format($accountSums[$account->id])}} 
Expected balance{{Amount::format($account->startBalance + $accountSums[$account->id])}} 
+
+
+ + + + +@stop +@section('scripts') +{{HTML::script('assets/javascript/firefly/reports.js')}} +@stop diff --git a/resources/views/reports/index.blade.php b/resources/views/reports/index.blade.php new file mode 100644 index 0000000000..13cc5236e3 --- /dev/null +++ b/resources/views/reports/index.blade.php @@ -0,0 +1,50 @@ +@extends('layouts.default') +@section('content') +{!! Breadcrumbs::renderIfExists(Route::getCurrentRoute()->getName()) !!} +
+
+
+
+ Yearly reports +
+
+
    + @foreach($years as $year) +
  • {{$year}}
  • + @endforeach +
+
+
+
+ +
+
+
+ Monthly reports +
+
+ +
+
+
+ +
+
+
+ Budget reports +
+
+ +
+
+
+
+@stop diff --git a/resources/views/reports/month.blade.php b/resources/views/reports/month.blade.php new file mode 100644 index 0000000000..c9cef65b18 --- /dev/null +++ b/resources/views/reports/month.blade.php @@ -0,0 +1,235 @@ +@extends('layouts.default') +@section('content') +{!! Breadcrumbs::renderIfExists(Route::getCurrentRoute()->getName(), $date) !!} +
+
+
+
Income
+ + + @foreach($income as $entry) + + + + + + + @endforeach + @if(isset($displaySum) && $displaySum === true) + + + + + + @endif +
+ @if($entry->encrypted === true) + {{{Crypt::decrypt($entry->description)}}} + @else + {{{$entry->description}}} + @endif + + amount);?> + @if($entry->type == 'Withdrawal') + {{Amount::format($entry->amount,false)}} + @endif + @if($entry->type == 'Deposit') + {{Amount::format($entry->amount,false)}} + @endif + @if($entry->type == 'Transfer') + {{Amount::format($entry->amount,false)}} + @endif + + {{$entry->date->format('j F Y')}} + + {{{$entry->name}}} +
Sum{!! Amount::format($tableSum) !!}
+
+
+
+
+
Expenses (top 10)
+ + + @foreach($expenses as $id => $expense) + + + @if($id > 0) + + @else + + @endif + + + @endforeach + + + + +
{{{$expense['name']}}}{{{$expense['name']}}}{!! Amount::format($expense['amount']) !!}
Sum{!! Amount::format($sum) !!}
+
+
+
+
+
Sums
+ transactions[1]->amount); + } + ?> + + + + + + + + + + + + + +
In{!! Amount::format($in) !!}
Out{!! Amount::format($sum) !!}
Difference{!! Amount::format($in - $sum) !!}
+
+
+
+
+
+
+
Budgets
+ + + + + + + + + @foreach($budgets as $id => $budget) + + + + + + + + @endforeach + + + + + + +
BudgetEnvelopeSpentLeft
+ @if($id > 0) + {{{$budget['name']}}} + @else + {{{$budget['name']}}} + @endif + {!! Amount::format($budget['amount']) !!}{!! Amount::format($budget['spent'],false) !!}{!! Amount::format($budget['amount'] + $budget['spent']) !!}
Sum{!! Amount::format($sumEnvelope) !!}{!! Amount::format($sumSpent) !!}{!! Amount::format($sumLeft) !!}
+
+
+
+
+
Categories
+ + + + + + + @foreach($categories as $id => $category) + + + + + + @endforeach + + + + +
CategorySpent
+ @if($id > 0) + {{{$category['name']}}} + @else + {{{$category['name']}}} + @endif + {!! Amount::format($category['amount'],false) !!}
Sum{!! Amount::format($sum) !!}
+
+
+
+
+
+
+
Accounts
+ + + @foreach($accounts as $id => $account) + + + + + + + + @endforeach + + + + + + +
{{{$account['name']}}}{!! Amount::format($account['startBalance']) !!}{!! Amount::format($account['endBalance']) !!}{!! Amount::format($account['difference']) !!}
Sum{!! Amount::format($sumStart) !!}{!! Amount::format($sumEnd) !!}{!! Amount::format($sumDiff) !!}
+
+
+
+
+
+
+
Piggy banks
+
Body
+
+
+
+
+
Repeated expenses
+
Body
+
+
+
+
+
+
+
Bills
+
Body
+
+
+
+
+
+
+
Outside of budgets
+
Body
+
+
+
+@stop diff --git a/resources/views/reports/year.blade.php b/resources/views/reports/year.blade.php new file mode 100644 index 0000000000..75ad2b879f --- /dev/null +++ b/resources/views/reports/year.blade.php @@ -0,0 +1,170 @@ +@extends('layouts.default') +@section('content') +{!! Breadcrumbs::renderIfExists(Route::getCurrentRoute()->getName(), $date) !!} +
+
+
+
+ Income vs. expenses +
+
+
+
+
+
+
+
+
+ Income vs. expenses +
+
+
+
+
+
+
+ +
+
+
+
+ Account balance +
+ + + @foreach($balances as $balance) + + + + + + + + @endforeach + + + + + + +
+ {{{$balance['account']->name}}} + @if($balance['shared']) + shared + @endif + {!! Amount::format($balance['start']) !!}{!! Amount::format($balance['end']) !!}{!! Amount::format($balance['end']-$balance['start']) !!}
Sum of sums{!! Amount::format($start) !!}{!! Amount::format($end) !!}{!! Amount::format($diff) !!}
+
+ +
+
+ Income vs. expense +
+ amount); + } + foreach($groupedExpenses as $exp) { + $expenseSum += floatval($exp['amount']); + } + $incomeSum = floatval($incomeSum*-1); + + ?> + + + + + + + + + + + + + + +
In{!! Amount::format($incomeSum) !!}
Out{!! Amount::format($expenseSum*-1) !!}
Difference{!! Amount::format($incomeSum - $expenseSum) !!}
+
+
+
+
+
+ Income +
+ + + @foreach($groupedIncomes as $income) + amount)*-1;?> + + + + + @endforeach + + + + +
{{{$income->name}}}{!! Amount::format(floatval($income->amount)*-1) !!}
Sum{!! Amount::format($sum) !!}
+
+
+
+
+
+ Expenses +
+ + + @foreach($groupedExpenses as $id => $expense) + + + + + + @endforeach + + + + +
{{{$expense['name']}}}{!! Amount::format(floatval($expense['amount'])*-1) !!}
Sum{!! Amount::format($sum) !!}
+
+
+
+
+
+
+
+ Budgets +
+
+
+
+
+
+
+ + +@stop +@section('scripts') + + + + + + + + + +@stop