From 33bf37315111531f8d73af3b81c6e0161f9fbab3 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 8 Apr 2016 14:50:18 +0200 Subject: [PATCH] First attempt at audit report (uses lots of queries). --- app/Http/Controllers/ReportController.php | 100 +++++++++++++-- .../Account/AccountRepository.php | 87 +++++++++++++ .../Account/AccountRepositoryInterface.php | 27 +++++ public/js/reports/audit/all.js | 114 ++++++++++++++++++ resources/lang/en_US/list.php | 6 + resources/views/list/journals.twig | 2 +- resources/views/reports/audit/report.twig | 65 +++++++--- .../reports/partials/journals-audit.twig | 96 +++++++++++++++ 8 files changed, 465 insertions(+), 32 deletions(-) create mode 100644 public/js/reports/audit/all.js create mode 100644 resources/views/reports/partials/journals-audit.twig diff --git a/app/Http/Controllers/ReportController.php b/app/Http/Controllers/ReportController.php index b416c7fa63..22e2657e42 100644 --- a/app/Http/Controllers/ReportController.php +++ b/app/Http/Controllers/ReportController.php @@ -7,11 +7,13 @@ use FireflyIII\Helpers\Report\BalanceReportHelperInterface; use FireflyIII\Helpers\Report\BudgetReportHelperInterface; use FireflyIII\Helpers\Report\ReportHelperInterface; use FireflyIII\Models\Account; +use FireflyIII\Models\Transaction; use FireflyIII\Repositories\Account\AccountRepositoryInterface as ARI; use Illuminate\Support\Collection; use Log; use Preferences; use Session; +use Steam; use View; /** @@ -128,24 +130,96 @@ class ReportController extends Controller return $this->defaultMonth($reportType, $start, $end, $accounts); case 'audit': - View::share( - 'subTitle', trans( - 'firefly.report_audit', - [ - 'start' => $start->formatLocalized($this->monthFormat), - 'end' => $end->formatLocalized($this->monthFormat), - ] - ) - ); - View::share('subTitleIcon', 'fa-calendar'); - - throw new FireflyException('Unfortunately, reports of the type "' . e($reportType) . '" are not yet available. '); - break; + return $this->auditReport($start, $end, $accounts); } } + /** + * @param Carbon $start + * @param Carbon $end + * @param Collection $accounts + * + * @return View + */ + private function auditReport(Carbon $start, Carbon $end, Collection $accounts) + { + bcscale(2); + /** @var ARI $repos */ + + $repos = app('FireflyIII\Repositories\Account\AccountRepositoryInterface'); + View::share( + 'subTitle', trans( + 'firefly.report_audit', + [ + 'start' => $start->formatLocalized($this->monthFormat), + 'end' => $end->formatLocalized($this->monthFormat), + ] + ) + ); + View::share('subTitleIcon', 'fa-calendar'); + + $auditData = []; + $dayBefore = clone $start; + $dayBefore->subDay(); + /** @var Account $account */ + foreach ($accounts as $account) { + // balance the day before: + $id = $account->id; + $first = $repos->oldestJournalDate($account); + $last = $repos->newestJournalDate($account); + $exists = false; + $journals = new Collection; + $dayBeforeBalance = Steam::balance($account, $dayBefore); + + if ($start->between($first, $last) || $end->between($first, $last)) { + $exists = true; + $journals = $repos->getJournalsInRange($account, $start, $end); + $journals = $journals->reverse(); + } + + + $startBalance = $dayBeforeBalance; + foreach ($journals as $journal) { + $journal->before = $startBalance; + + // get currently relevant transaction: + $transaction = $journal->transactions->filter( + function (Transaction $t) use ($account) { + return $t->account_id === $account->id; + } + )->first(); + + $newBalance = bcadd($startBalance, $transaction->amount); + $journal->after = $newBalance; + $startBalance = $newBalance; + + } + + $journals = $journals->reverse(); + + + $auditData[$id]['journals'] = $journals; + $auditData[$id]['exists'] = $exists; + $auditData[$id]['end'] = $end->formatLocalized(trans('config.month_and_day')); + $auditData[$id]['endBalance'] = Steam::balance($account, $end); + $auditData[$id]['dayBefore'] = $dayBefore->formatLocalized(trans('config.month_and_day')); + $auditData[$id]['dayBeforeBalance'] = $dayBeforeBalance; + } + + + $reportType = 'audit'; + $accountIds = join(',', $accounts->pluck('id')->toArray()); + + $hideable = ['buttons', 'icon', 'description', 'balance_before', 'amount', 'balance_after', 'date', 'book_date', 'process_date', 'interest_date', + 'from', 'to', 'budget', 'category', 'bill', 'create_date', 'update_date', + ]; + $defaultShow = ['icon', 'description', 'balance_before', 'amount', 'balance_after', 'date', 'to']; + + return view('reports.audit.report', compact('start', 'end', 'reportType', 'accountIds', 'accounts', 'auditData', 'hideable', 'defaultShow')); + } + /** * @param $reportType * @param Carbon $start diff --git a/app/Repositories/Account/AccountRepository.php b/app/Repositories/Account/AccountRepository.php index 6d29a6119b..a30710465d 100644 --- a/app/Repositories/Account/AccountRepository.php +++ b/app/Repositories/Account/AccountRepository.php @@ -280,6 +280,43 @@ class AccountRepository implements AccountRepositoryInterface } + /** + * @param Account $account + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + public function getJournalsInRange(Account $account, Carbon $start, Carbon $end): Collection + { + $query = $this->user + ->transactionJournals() + ->expanded() + ->with( + [ + 'transactions' => function (HasMany $q) { + $q->orderBy('amount', 'ASC'); + }, + 'transactionType', + 'transactionCurrency', + 'budgets', + 'categories', + 'bill', + ] + ) + ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->where('transactions.account_id', $account->id) + ->after($start) + ->before($end) + ->orderBy('transaction_journals.date', 'DESC') + ->orderBy('transaction_journals.order', 'ASC') + ->orderBy('transaction_journals.id', 'DESC'); + + $set = $query->get(TransactionJournal::QUERYFIELDS); + + return $set; + } + /** * Get the accounts of a user that have piggy banks connected to them. * @@ -387,6 +424,56 @@ class AccountRepository implements AccountRepositoryInterface } + /** + * Returns the date of the very last transaction in this account. + * + * @param Account $account + * + * @return Carbon + */ + public function newestJournalDate(Account $account): Carbon + { + /** @var TransactionJournal $journal */ + $journal = TransactionJournal:: + leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->where('transactions.account_id', $account->id) + ->orderBy('transaction_journals.date', 'ASC') + ->first(['transaction_journals.*']); + if (is_null($journal)) { + $date = new Carbon; + $date->addYear(); // in the future. + } else { + $date = $journal->date; + } + + return $date; + } + + /** + * Returns the date of the very first transaction in this account. + * + * @param Account $account + * + * @return Carbon + */ + public function oldestJournalDate(Account $account): Carbon + { + /** @var TransactionJournal $journal */ + $journal = TransactionJournal:: + leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->where('transactions.account_id', $account->id) + ->orderBy('transaction_journals.date', 'DESC') + ->first(['transaction_journals.*']); + if (is_null($journal)) { + $date = new Carbon; + $date->addYear(); // in the future. + } else { + $date = $journal->date; + } + + return $date; + } + /** * @param Account $account * diff --git a/app/Repositories/Account/AccountRepositoryInterface.php b/app/Repositories/Account/AccountRepositoryInterface.php index 4b32a75b48..68fb208b0b 100644 --- a/app/Repositories/Account/AccountRepositoryInterface.php +++ b/app/Repositories/Account/AccountRepositoryInterface.php @@ -129,6 +129,15 @@ interface AccountRepositoryInterface */ public function getJournals(Account $account, int $page): LengthAwarePaginator; + /** + * @param Account $account + * @param Carbon $start + * @param Carbon $end + * + * @return Collection + */ + public function getJournalsInRange(Account $account, Carbon $start, Carbon $end): Collection; + /** * Get the accounts of a user that have piggy banks connected to them. * @@ -151,6 +160,24 @@ interface AccountRepositoryInterface */ public function leftOnAccount(Account $account, Carbon $date): string; + /** + * Returns the date of the very last transaction in this account. + * + * @param Account $account + * + * @return Carbon + */ + public function newestJournalDate(Account $account): Carbon; + + /** + * Returns the date of the very first transaction in this account. + * + * @param Account $account + * + * @return Carbon + */ + public function oldestJournalDate(Account $account): Carbon; + /** * @param Account $account * diff --git a/public/js/reports/audit/all.js b/public/js/reports/audit/all.js new file mode 100644 index 0000000000..c57875937c --- /dev/null +++ b/public/js/reports/audit/all.js @@ -0,0 +1,114 @@ +/* + * all.js + * Copyright (C) 2016 thegrumpydictator@gmail.com + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +/* globals hideable */ + + +/** + * Created by sander on 01/04/16. + */ + +$(function () { + "use strict"; + + // scan current selection of checkboxes and put them in a cookie: + var arr; + if ((readCookie('audit-option-checkbox') !== null)) { + arr = readCookie('audit-option-checkbox').split(','); + arr.forEach(function (val) { + $('input[type="checkbox"][value="' + val + '"]').prop('checked', true); + }); + console.log('arr from cookie is ' + arr) + } else { + // no cookie? read list, store in array 'arr' + // all account ids: + arr = readCheckboxes(); + } + storeCheckboxes(arr); + + + // process options: + showOnlyColumns(arr); + + // respond to click each button: + $('.audit-option-checkbox').click(clickColumnOption); + +}); + +function clickColumnOption() { + "use strict"; + var newArr = readCheckboxes(); + showOnlyColumns(newArr); + storeCheckboxes(newArr); +} + +function storeCheckboxes(checkboxes) { + "use strict"; + // store new cookie with those options: + console.log('Store new cookie with those options: ' + checkboxes); + createCookie('audit-option-checkbox', checkboxes, 365); +} + +function readCheckboxes() { + "use strict"; + var checkboxes = []; + $.each($('.audit-option-checkbox'), function (i, v) { + var c = $(v); + if (c.prop('checked')) { + //url += c.val() + ','; + checkboxes.push(c.val()); + } + }); + console.log('arr is now (default): ' + checkboxes); + return checkboxes; +} + +function showOnlyColumns(checkboxes) { + "use strict"; + + for (var i = 0; i < hideable.length; i++) { + var opt = hideable[i]; + if(checkboxes.indexOf(opt) > -1) { + console.log(opt + ' is in checkboxes'); + $('td.hide-' + opt).show(); + $('th.hide-' + opt).show(); + } else { + console.log(opt + ' is NOT in checkboxes'); + $('th.hide-' + opt).hide(); + $('td.hide-' + opt).hide(); + } + } +} + + +function createCookie(name, value, days) { + "use strict"; + var expires; + + if (days) { + var date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + expires = "; expires=" + date.toGMTString(); + } else { + expires = ""; + } + document.cookie = encodeURIComponent(name) + "=" + encodeURIComponent(value) + expires + "; path=/"; +} + +function readCookie(name) { + "use strict"; + var nameEQ = encodeURIComponent(name) + "="; + var ca = document.cookie.split(';'); + for (var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) === ' ') c = c.substring(1, c.length); + if (c.indexOf(nameEQ) === 0) return decodeURIComponent(c.substring(nameEQ.length, c.length)); + } + return null; +} + diff --git a/resources/lang/en_US/list.php b/resources/lang/en_US/list.php index 9aaccbe322..ca4ab77a2d 100644 --- a/resources/lang/en_US/list.php +++ b/resources/lang/en_US/list.php @@ -8,6 +8,12 @@ */ return [ + 'buttons' => 'Buttons', + 'icon' => 'Icon', + 'create_date' => 'Created at', + 'update_date' => 'Updated at', + 'balance_before' => 'Balance before', + 'balance_after' => 'Balance after', 'name' => 'Name', 'role' => 'Role', 'currentBalance' => 'Current balance', diff --git a/resources/views/list/journals.twig b/resources/views/list/journals.twig index 7115ade1b0..9e60834eb4 100644 --- a/resources/views/list/journals.twig +++ b/resources/views/list/journals.twig @@ -1,6 +1,6 @@ {{ journals.render|raw }} - +
diff --git a/resources/views/reports/audit/report.twig b/resources/views/reports/audit/report.twig index adc37cf7ba..682658ad32 100644 --- a/resources/views/reports/audit/report.twig +++ b/resources/views/reports/audit/report.twig @@ -6,6 +6,28 @@ {% block content %} + +
+
+
+
+

{{ 'options'|_ }}

+
+
+
    + {% for hide in hideable %} +
  • + {% endfor %} + +
+
+
+
+
+ + {% for account in accounts %}
@@ -13,31 +35,34 @@

{{ account.name }}

-
- {% if not auditData[account.id].exists %} + + {% if not auditData[account.id].exists %} +
No activity was recorded on account {{ account.name }} - between + between {{ start }} and {{ end }}. - {% else %} -

- Account balance of {{ account.name }} - at the end of {{ auditData[account.id].end }} was: - {{ auditData[account.id].endBalance|formatAmount }} -

- {% include 'list/journals-extended.twig' with {'journals': auditData[account.id].journals,'account':account} %} -

- Account balance of {{ account.name }} - at the end of {{ auditData[account.id].dayBefore }} was: - {{ auditData[account.id].dayBeforeBalance|formatAmount }} -

- {% endif %} -
+
+ {% else %} +
+

+ Account balance of {{ account.name }} + at the end of {{ auditData[account.id].end }} was: + {{ auditData[account.id].endBalance|formatAmount }} +

+ {% include 'reports/partials/journals-audit.twig' with {'journals': auditData[account.id].journals,'account':account} %} +

+ Account balance of {{ account.name }} + at the end of {{ auditData[account.id].dayBefore }} was: + {{ auditData[account.id].dayBeforeBalance|formatAmount }} +

+
+ {% endif %}
@@ -47,4 +72,8 @@ {% block styles %} {% endblock %} {% block scripts %} + + {% endblock %} diff --git a/resources/views/reports/partials/journals-audit.twig b/resources/views/reports/partials/journals-audit.twig new file mode 100644 index 0000000000..dd47064971 --- /dev/null +++ b/resources/views/reports/partials/journals-audit.twig @@ -0,0 +1,96 @@ +{{ journals.render|raw }} + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + {% for journal in journals %} + + + + + + + + + + + + + + + + + + + {% if journal.budgets[0] %} + + {% else %} + + {% endif %} + {% if journal.categories[0] %} + + {% else %} + + {% endif %} + {% if journal.bill_id %} + + {% else %} + + {% endif %} + + + + + + + {% endfor %} + +
  {{ trans('list.description') }}{{ trans('list.balance_before') }}{{ trans('list.amount') }}{{ trans('list.balance_after') }}{{ trans('list.date') }}{{ trans('list.book_date') }}{{ trans('list.process_date') }}{{ trans('list.interest_date') }}{{ trans('list.from') }}{{ trans('list.to') }}{{ trans('list.bill') }}{{ trans('list.create_date') }}{{ trans('list.update_date') }}
+
+ +
{{ journal|typeIcon }}{{ journal.description }}{{ journal.before|formatAmount }}{{ journal|formatJournal }}{{ journal.after|formatAmount }}{{ journal.date.formatLocalized(monthAndDayFormat) }}{{ journal.book_date.formatLocalized(monthAndDayFormat) }}{{ journal.process_date.formatLocalized(monthAndDayFormat) }}{{ journal.interest_date.formatLocalized(monthAndDayFormat) }} + {% if journal.source_account_type == 'Cash account' %} + (cash) + {% else %} + {{ journal.source_account_name }} + {% endif %} + + {% if journal.destination_account_type == 'Cash account' %} + (cash) + {% else %} + {{ journal.destination_account_name }} + {% endif %} + {{ journal.budgets[0].name }}{{ 'no_budget'|_ }}{{ journal.categories[0].name }}{{ 'no_category'|_ }} {{ journal.bill.name }}  + {{ journal.created_at.formatLocalized(dateTimeFormat) }} + + {{ journal.updated_at.formatLocalized(dateTimeFormat) }} +
+ +{{ journals.render|raw }}