From 0f0f91237093ace58c2c04757447f810705edf74 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 12 May 2017 06:21:26 +0200 Subject: [PATCH 001/126] Partial JS focus [skip ci] --- public/js/ff/transactions/single/create.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/public/js/ff/transactions/single/create.js b/public/js/ff/transactions/single/create.js index 56109f855c..f74263d899 100644 --- a/public/js/ff/transactions/single/create.js +++ b/public/js/ff/transactions/single/create.js @@ -68,7 +68,9 @@ function updateNativeCurrency() { $('.currency-option[data-id="' + nativeCurrencyId + '"]').click(); $('[data-toggle="dropdown"]').parent().removeClass('open'); - //$('select[name="source_account_id"]').focus(); + if (what !== 'transfer') { + $('select[name="source_account_id"]').focus(); + } validateCurrencyForTransfer(); } From 2eafd3cc15acfd8c581fb35f6cabcc6cfcad1117 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 14 May 2017 08:57:43 +0200 Subject: [PATCH 002/126] Should fix #644 --- app/Console/Commands/UpgradeDatabase.php | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/app/Console/Commands/UpgradeDatabase.php b/app/Console/Commands/UpgradeDatabase.php index d313c55ec6..aa2656b712 100644 --- a/app/Console/Commands/UpgradeDatabase.php +++ b/app/Console/Commands/UpgradeDatabase.php @@ -269,9 +269,10 @@ class UpgradeDatabase extends Command $repository = app(CurrencyRepositoryInterface::class); $notification = '%s #%d uses %s but should use %s. It has been updated. Please verify this in Firefly III.'; $transfer = 'Transfer #%d has been updated to use the correct currencies. Please verify this in Firefly III.'; + $driver = DB::connection()->getDriverName(); foreach ($types as $type => $operator) { - $set = TransactionJournal + $query = TransactionJournal ::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')->leftJoin( 'transactions', function (JoinClause $join) use ($operator) { $join->on('transaction_journals.id', '=', 'transactions.transaction_journal_id')->where('transactions.amount', $operator, '0'); @@ -280,9 +281,15 @@ class UpgradeDatabase extends Command ->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id') ->leftJoin('account_meta', 'account_meta.account_id', '=', 'accounts.id') ->where('transaction_types.type', $type) - ->where('account_meta.name', 'currency_id') - ->where('transaction_journals.transaction_currency_id', '!=', DB::raw('account_meta.data')) - ->get(['transaction_journals.*', 'account_meta.data as expected_currency_id', 'transactions.amount as transaction_amount']); + ->where('account_meta.name', 'currency_id'); + if($driver === 'postgresql') { + $query->where('transaction_journals.transaction_currency_id', '!=', DB::raw('cast(account_meta.data as int)')); + } + if($driver !== 'postgresql') { + $query->where('transaction_journals.transaction_currency_id', '!=', DB::raw('account_meta.data')); + } + + $set = $query->get(['transaction_journals.*', 'account_meta.data as expected_currency_id', 'transactions.amount as transaction_amount']); /** @var TransactionJournal $journal */ foreach ($set as $journal) { $expectedCurrency = $repository->find(intval($journal->expected_currency_id)); From 01fedc0bf8d1d71608dc1d4b613865f156517318 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 2 Jun 2017 06:45:38 +0200 Subject: [PATCH 003/126] Fix for #593, as inspired by @nhaarman. --- .../Controllers/Chart/BudgetController.php | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/app/Http/Controllers/Chart/BudgetController.php b/app/Http/Controllers/Chart/BudgetController.php index 189c1e2dea..8a0d923231 100644 --- a/app/Http/Controllers/Chart/BudgetController.php +++ b/app/Http/Controllers/Chart/BudgetController.php @@ -31,6 +31,7 @@ use Illuminate\Support\Collection; use Navigation; use Preferences; use Response; +use Steam; /** * Class BudgetController @@ -320,12 +321,12 @@ class BudgetController extends Controller ['label' => strval(trans('firefly.overspent')), 'entries' => [], 'type' => 'bar',], ]; - /** @var Budget $budget */ foreach ($budgets as $budget) { // get relevant repetitions: $limits = $this->repository->getBudgetLimits($budget, $start, $end); $expenses = $this->getExpensesForBudget($limits, $budget, $start, $end); + foreach ($expenses as $name => $row) { $chartData[0]['entries'][$name] = $row['spent']; $chartData[1]['entries'][$name] = $row['left']; @@ -529,9 +530,7 @@ class BudgetController extends Controller $rows = $this->spentInPeriodMulti($budget, $limits); foreach ($rows as $name => $row) { if (bccomp($row['spent'], '0') !== 0 || bccomp($row['left'], '0') !== 0) { - $return[$name]['spent'] = bcmul($row['spent'], '-1'); - $return[$name]['left'] = $row['left']; - $return[$name]['overspent'] = bcmul($row['overspent'], '-1'); + $return[$name] = $row; } } unset($rows, $row); @@ -563,6 +562,7 @@ class BudgetController extends Controller /** @var BudgetLimit $budgetLimit */ foreach ($limits as $budgetLimit) { $expenses = $this->repository->spentInPeriod(new Collection([$budget]), new Collection, $budgetLimit->start_date, $budgetLimit->end_date); + $expenses = Steam::positive($expenses); if ($limits->count() > 1) { $name = $budget->name . ' ' . trans( @@ -578,10 +578,14 @@ class BudgetController extends Controller * left: amount of budget limit min spent, or 0 when < 0. * spent: spent, or amount of budget limit when > amount */ - $amount = $budgetLimit->amount; - $left = bccomp(bcadd($amount, $expenses), '0') < 1 ? '0' : bcadd($amount, $expenses); - $spent = bccomp($expenses, $amount) === 1 ? $expenses : bcmul($amount, '-1'); - $overspent = bccomp(bcadd($amount, $expenses), '0') < 1 ? bcadd($amount, $expenses) : '0'; + $amount = $budgetLimit->amount; + $leftInLimit = bcsub($amount, $expenses); + $hasOverspent = bccomp($leftInLimit, '0') === -1; + + $left = $hasOverspent ? '0' : bcsub($amount, $expenses); + $spent = $hasOverspent ? $amount : $expenses; + $overspent = $hasOverspent ? Steam::positive($leftInLimit) : '0'; + $return[$name] = [ 'left' => $left, 'overspent' => $overspent, From 2b1ab5c6ef9689bdd6c02eae9e89be6e916616fa Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 2 Jun 2017 07:05:42 +0200 Subject: [PATCH 004/126] Fixed edit of multi currency transaction, ##651 --- app/Repositories/Journal/JournalRepository.php | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/app/Repositories/Journal/JournalRepository.php b/app/Repositories/Journal/JournalRepository.php index 98ab2dafb5..ef4820ac2e 100644 --- a/app/Repositories/Journal/JournalRepository.php +++ b/app/Repositories/Journal/JournalRepository.php @@ -259,18 +259,9 @@ class JournalRepository implements JournalRepositoryInterface $journal->description = $data['description']; $journal->date = $data['date']; $accounts = $this->storeAccounts($journal->transactionType, $data); + $data = $this->verifyNativeAmount($data, $accounts); $amount = strval($data['amount']); - if ($data['currency_id'] !== $journal->transaction_currency_id) { - // user has entered amount in foreign currency. - // amount in "our" currency is $data['exchanged_amount']: - $amount = strval($data['exchanged_amount']); - // other values must be stored as well: - $data['original_amount'] = $data['amount']; - $data['original_currency_id'] = $data['currency_id']; - - } - // unlink all categories, recreate them: $journal->categories()->detach(); $journal->budgets()->detach(); From 8cdc1f00140e4b67bc26fd5c69efaa5118aaae37 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 2 Jun 2017 12:59:14 +0200 Subject: [PATCH 005/126] Rename several twig files. --- app/Http/Controllers/RuleController.php | 2 +- resources/views/accounts/show.twig | 2 +- resources/views/bills/show.twig | 2 +- resources/views/budgets/no-budget.twig | 2 +- resources/views/budgets/show.twig | 2 +- resources/views/categories/no-category.twig | 2 +- resources/views/categories/show.twig | 2 +- resources/views/index.twig | 2 +- .../list/{journals-tiny-tasker.twig => journals-tiny.twig} | 0 resources/views/list/{journals-tasker.twig => journals.twig} | 0 .../views/popup/list/{journals-tasker.twig => journals.twig} | 0 resources/views/popup/report/balance-amount.twig | 2 +- resources/views/popup/report/budget-spent-amount.twig | 2 +- resources/views/popup/report/category-entry.twig | 2 +- resources/views/popup/report/expense-entry.twig | 2 +- resources/views/popup/report/income-entry.twig | 2 +- resources/views/reports/audit/report.twig | 2 +- .../{journals-audit-tasker.twig => journals-audit.twig} | 0 resources/views/tags/show.twig | 2 +- resources/views/transactions/index.twig | 2 +- 20 files changed, 16 insertions(+), 16 deletions(-) rename resources/views/list/{journals-tiny-tasker.twig => journals-tiny.twig} (100%) rename resources/views/list/{journals-tasker.twig => journals.twig} (100%) rename resources/views/popup/list/{journals-tasker.twig => journals.twig} (100%) rename resources/views/reports/partials/{journals-audit-tasker.twig => journals-audit.twig} (100%) diff --git a/app/Http/Controllers/RuleController.php b/app/Http/Controllers/RuleController.php index a7247b7705..ba4dc6d208 100644 --- a/app/Http/Controllers/RuleController.php +++ b/app/Http/Controllers/RuleController.php @@ -306,7 +306,7 @@ class RuleController extends Controller } // Return json response - $view = view('list.journals-tiny-tasker', ['transactions' => $matchingTransactions])->render(); + $view = view('list.journals-tiny', ['transactions' => $matchingTransactions])->render(); return Response::json(['html' => $view, 'warning' => $warning]); } diff --git a/resources/views/accounts/show.twig b/resources/views/accounts/show.twig index bedb9ee721..40da0b960c 100644 --- a/resources/views/accounts/show.twig +++ b/resources/views/accounts/show.twig @@ -86,7 +86,7 @@

{{ 'transactions'|_ }}

- {% include 'list.journals-tasker' with {sorting:true, hideBills:true, hideBudgets: true, hideCategories: true} %} + {% include 'list.journals' with {sorting:true, hideBills:true, hideBudgets: true, hideCategories: true} %} {% if periods.count > 0 %}

diff --git a/resources/views/bills/show.twig b/resources/views/bills/show.twig index 2b01b46c29..7e81359214 100644 --- a/resources/views/bills/show.twig +++ b/resources/views/bills/show.twig @@ -106,7 +106,7 @@

{{ 'connected_journals'|_ }}

- {% include 'list.journals-tasker' %} + {% include 'list.journals' %}
diff --git a/resources/views/budgets/no-budget.twig b/resources/views/budgets/no-budget.twig index fd23bde560..c2311cb332 100644 --- a/resources/views/budgets/no-budget.twig +++ b/resources/views/budgets/no-budget.twig @@ -22,7 +22,7 @@

{{ subTitle }}

- {% include 'list.journals-tasker' with {'journals': journals,'hideBudgets': true} %} + {% include 'list.journals' with {'journals': journals,'hideBudgets': true} %} {% if periods.count > 0 %}

diff --git a/resources/views/budgets/show.twig b/resources/views/budgets/show.twig index 3e598b1d6e..824efb1ce2 100644 --- a/resources/views/budgets/show.twig +++ b/resources/views/budgets/show.twig @@ -88,7 +88,7 @@

{{ 'transactions'|_ }}

- {% include 'list.journals-tasker' with {hideBudgets:true, hideBills:true} %} + {% include 'list.journals' with {hideBudgets:true, hideBills:true} %} {% if budgetLimit %}

diff --git a/resources/views/categories/no-category.twig b/resources/views/categories/no-category.twig index c6ebc131f4..0037b77257 100644 --- a/resources/views/categories/no-category.twig +++ b/resources/views/categories/no-category.twig @@ -22,7 +22,7 @@

{{ subTitle }}

- {% include 'list.journals-tasker' with {'journals': journals, 'hideCategories':true} %} + {% include 'list.journals' with {'journals': journals, 'hideCategories':true} %} {% if periods.count > 0 %}

diff --git a/resources/views/categories/show.twig b/resources/views/categories/show.twig index 186ad996de..9a1648d3da 100644 --- a/resources/views/categories/show.twig +++ b/resources/views/categories/show.twig @@ -65,7 +65,7 @@

{{ 'transactions'|_ }}

- {% include 'list.journals-tasker' with {hideCategories: true} %} + {% include 'list.journals' with {hideCategories: true} %} {% if periods.count > 0 %}

diff --git a/resources/views/index.twig b/resources/views/index.twig index 7dd28358b9..6cd8914de2 100644 --- a/resources/views/index.twig +++ b/resources/views/index.twig @@ -85,7 +85,7 @@ {% if data[0].count > 0 %}

- {% include 'list.journals-tiny-tasker' with {'transactions': data[0],'account': data[1]} %} + {% include 'list.journals-tiny' with {'transactions': data[0],'account': data[1]} %}
{% else %}
diff --git a/resources/views/list/journals-tiny-tasker.twig b/resources/views/list/journals-tiny.twig similarity index 100% rename from resources/views/list/journals-tiny-tasker.twig rename to resources/views/list/journals-tiny.twig diff --git a/resources/views/list/journals-tasker.twig b/resources/views/list/journals.twig similarity index 100% rename from resources/views/list/journals-tasker.twig rename to resources/views/list/journals.twig diff --git a/resources/views/popup/list/journals-tasker.twig b/resources/views/popup/list/journals.twig similarity index 100% rename from resources/views/popup/list/journals-tasker.twig rename to resources/views/popup/list/journals.twig diff --git a/resources/views/popup/report/balance-amount.twig b/resources/views/popup/report/balance-amount.twig index 1f5bd820bb..426aab6d07 100644 --- a/resources/views/popup/report/balance-amount.twig +++ b/resources/views/popup/report/balance-amount.twig @@ -10,7 +10,7 @@
- {% include 'list/journals-tasker' %} + {% include 'list/journals' %} {% if periods %}

diff --git a/resources/views/transactions/index.twig b/resources/views/transactions/index.twig index 4a72c0cb2f..65bfbf64e7 100644 --- a/resources/views/transactions/index.twig +++ b/resources/views/transactions/index.twig @@ -22,7 +22,7 @@

{{ subTitle }}

- {% include 'list.journals-tasker' with {'journals': journals} %} + {% include 'list.journals' with {'journals': journals} %} {% if periods.count > 0 %}

From ec1507d644d348c7682530ba0be04899d8ece354 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 2 Jun 2017 12:59:27 +0200 Subject: [PATCH 006/126] Add some documentation [skip ci] --- app/Generator/Chart/Basic/GeneratorInterface.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/app/Generator/Chart/Basic/GeneratorInterface.php b/app/Generator/Chart/Basic/GeneratorInterface.php index 92b1b29d93..31deeb0a30 100644 --- a/app/Generator/Chart/Basic/GeneratorInterface.php +++ b/app/Generator/Chart/Basic/GeneratorInterface.php @@ -22,10 +22,15 @@ interface GeneratorInterface { /** - * Will generate a (ChartJS) compatible array from the given input. Expects this format: + * Will generate a Chart JS compatible array from the given input. Expects this format + * + * Will take labels for all from first set. * * 0: [ * 'label' => 'label of set', + * 'type' => bar or line, optional + * 'yAxisID' => ID of yAxis, optional, will not be included when unused. + * 'fill' => if to fill a line? optional, will not be included when unused. * 'entries' => * [ * 'label-of-entry' => 'value' @@ -33,12 +38,16 @@ interface GeneratorInterface * ] * 1: [ * 'label' => 'label of another set', + * 'type' => bar or line, optional + * 'yAxisID' => ID of yAxis, optional, will not be included when unused. + * 'fill' => if to fill a line? optional, will not be included when unused. * 'entries' => * [ * 'label-of-entry' => 'value' * ] * ] * + * @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's five. * * @param array $data * From c05f34437114fba7ad9c8d75f48a52ac1e1765ae Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 2 Jun 2017 13:00:09 +0200 Subject: [PATCH 007/126] Code clean up [skip ci] --- app/Http/Controllers/Transaction/SingleController.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/Http/Controllers/Transaction/SingleController.php b/app/Http/Controllers/Transaction/SingleController.php index 27c7cdba77..eef9bd7ab4 100644 --- a/app/Http/Controllers/Transaction/SingleController.php +++ b/app/Http/Controllers/Transaction/SingleController.php @@ -250,8 +250,8 @@ class SingleController extends Controller 'source_account_name' => $sourceAccounts->first()->edit_name, 'destination_account_id' => $destinationAccounts->first()->id, 'destination_account_name' => $destinationAccounts->first()->edit_name, - 'amount' => $journal->amountPositive(), - 'currency' => $journal->transactionCurrency, + 'amount' => $journal->amountPositive(), + 'currency' => $journal->transactionCurrency, // new custom fields: 'due_date' => $journal->dateAsString('due_date'), @@ -261,8 +261,8 @@ class SingleController extends Controller 'notes' => $journal->getMeta('notes'), // exchange rate fields - 'native_amount' => $journal->amountPositive(), - 'native_currency' => $journal->transactionCurrency, + 'native_amount' => $journal->amountPositive(), + 'native_currency' => $journal->transactionCurrency, ]; // if user has entered a foreign currency, update some fields From 74664afa682b79099f4e02eb569207f05c7f259a Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 2 Jun 2017 13:00:24 +0200 Subject: [PATCH 008/126] Was not able to remove opening balance. --- .../Account/AccountRepository.php | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/app/Repositories/Account/AccountRepository.php b/app/Repositories/Account/AccountRepository.php index 423efcd23e..122e8f7d5c 100644 --- a/app/Repositories/Account/AccountRepository.php +++ b/app/Repositories/Account/AccountRepository.php @@ -466,7 +466,12 @@ class AccountRepository implements AccountRepositoryInterface */ protected function storeInitialBalance(Account $account, array $data): TransactionJournal { - $amount = $data['openingBalance']; + $amount = strval($data['openingBalance']); + + if (bccomp($amount, '0') === 0) { + return new TransactionJournal; + } + $name = $data['name']; $currencyId = $data['currency_id']; $opposing = $this->storeOpposingAccount($name); @@ -487,12 +492,12 @@ class AccountRepository implements AccountRepositoryInterface $firstAccount = $account; $secondAccount = $opposing; $firstAmount = $amount; - $secondAmount = $amount * -1; + $secondAmount = bcmul($amount, '-1'); if ($data['openingBalance'] < 0) { $firstAccount = $opposing; $secondAccount = $account; - $firstAmount = $amount * -1; + $firstAmount = bcmul($amount, '-1'); $secondAmount = $amount; } @@ -606,9 +611,15 @@ class AccountRepository implements AccountRepositoryInterface protected function updateOpeningBalanceJournal(Account $account, TransactionJournal $journal, array $data): bool { $date = $data['openingBalanceDate']; - $amount = $data['openingBalance']; + $amount = strval($data['openingBalance']); $currencyId = intval($data['currency_id']); + if (bccomp($amount, '0') === 0) { + $journal->delete(); + + return true; + } + // update date: $journal->date = $date; $journal->transaction_currency_id = $currencyId; @@ -621,7 +632,7 @@ class AccountRepository implements AccountRepositoryInterface $transaction->save(); } if ($account->id != $transaction->account_id) { - $transaction->amount = $amount * -1; + $transaction->amount = bcmul($amount, '-1'); $transaction->save(); } } @@ -631,6 +642,7 @@ class AccountRepository implements AccountRepositoryInterface } + /** * @param array $data * @@ -638,9 +650,7 @@ class AccountRepository implements AccountRepositoryInterface */ protected function validOpeningBalanceData(array $data): bool { - if (isset($data['openingBalance']) && isset($data['openingBalanceDate']) - && bccomp(strval($data['openingBalance']), '0') !== 0 - ) { + if (isset($data['openingBalance']) && isset($data['openingBalanceDate'])) { Log::debug('Array has valid opening balance data.'); return true; From 8273f467b6511f40e90d327ed0f78ad5cbd0abc0 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 2 Jun 2017 13:00:43 +0200 Subject: [PATCH 009/126] Refactor some JS functions. --- public/js/ff/charts.js | 6 +++--- public/js/ff/reports/category/month.js | 20 +++++++++++--------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/public/js/ff/charts.js b/public/js/ff/charts.js index 35b99f0b00..fa0c8f049a 100644 --- a/public/js/ff/charts.js +++ b/public/js/ff/charts.js @@ -75,7 +75,7 @@ function lineChart(URI, container) { "use strict"; var colorData = true; - var options = defaultChartOptions; + var options = $.extend(true, {}, defaultChartOptions); var chartType = 'line'; drawAChart(URI, container, chartType, options, colorData); @@ -188,7 +188,7 @@ function columnChart(URI, container) { "use strict"; console.log('Going to draw column chart for ' + URI + ' in ' + container); var colorData = true; - var options = defaultChartOptions; + var options = $.extend(true, {}, defaultChartOptions); var chartType = 'bar'; drawAChart(URI, container, chartType, options, colorData); @@ -224,7 +224,7 @@ function pieChart(URI, container) { "use strict"; var colorData = false; - var options = defaultPieOptions; + var options = $.extend(true, {}, defaultPieOptions); var chartType = 'pie'; drawAChart(URI, container, chartType, options, colorData); diff --git a/public/js/ff/reports/category/month.js b/public/js/ff/reports/category/month.js index 84f3444d9b..e803ff3de7 100644 --- a/public/js/ff/reports/category/month.js +++ b/public/js/ff/reports/category/month.js @@ -15,19 +15,19 @@ $(function () { drawChart(); $('#categories-in-pie-chart-checked').on('change', function () { - redrawPieChart('categories-in-pie-chart', categoryIncomeUri); + redrawPieChart(categoryIncomeUri, 'categories-in-pie-chart'); }); $('#categories-out-pie-chart-checked').on('change', function () { - redrawPieChart('categories-out-pie-chart', categoryExpenseUri); + redrawPieChart(categoryExpenseUri, 'categories-out-pie-chart'); }); $('#accounts-in-pie-chart-checked').on('change', function () { - redrawPieChart('accounts-in-pie-chart', accountIncomeUri); + redrawPieChart(accountIncomeUri, 'accounts-in-pie-chart'); }); $('#accounts-out-pie-chart-checked').on('change', function () { - redrawPieChart('accounts-out-pie-chart', accountExpenseUri); + redrawPieChart(accountExpenseUri, 'accounts-out-pie-chart'); }); }); @@ -40,15 +40,17 @@ function drawChart() { doubleYChart(mainUri, 'in-out-chart'); // draw pie chart of income, depending on "show other transactions too": - redrawPieChart('categories-in-pie-chart', categoryIncomeUri); - redrawPieChart('categories-out-pie-chart', categoryExpenseUri); - redrawPieChart('accounts-in-pie-chart', accountIncomeUri); - redrawPieChart('accounts-out-pie-chart', accountExpenseUri); + redrawPieChart(categoryIncomeUri, 'categories-in-pie-chart'); + redrawPieChart(categoryExpenseUri, 'categories-out-pie-chart'); + redrawPieChart(accountIncomeUri, 'accounts-in-pie-chart'); + redrawPieChart(accountExpenseUri, 'accounts-out-pie-chart'); + stackedColumnChart(expenseAccountTimeUri, 'expense-time-chart'); + stackedColumnChart(revenueAccountTimeUri, 'revenue-time-chart'); } -function redrawPieChart(container, uri) { +function redrawPieChart(uri, container) { "use strict"; var checkbox = $('#' + container + '-checked'); From e1aebbe12b550cd91cd4891a3028d1b52e8837a2 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 2 Jun 2017 13:01:24 +0200 Subject: [PATCH 010/126] Remove non-existing charts. --- public/js/ff/reports/category/month.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/public/js/ff/reports/category/month.js b/public/js/ff/reports/category/month.js index e803ff3de7..b07941a944 100644 --- a/public/js/ff/reports/category/month.js +++ b/public/js/ff/reports/category/month.js @@ -45,9 +45,6 @@ function drawChart() { redrawPieChart(accountIncomeUri, 'accounts-in-pie-chart'); redrawPieChart(accountExpenseUri, 'accounts-out-pie-chart'); - stackedColumnChart(expenseAccountTimeUri, 'expense-time-chart'); - stackedColumnChart(revenueAccountTimeUri, 'revenue-time-chart'); - } function redrawPieChart(uri, container) { From 4ff5f33966a8480820e6a32ebc5c3debef3c84d0 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 2 Jun 2017 13:01:43 +0200 Subject: [PATCH 011/126] Prep for solid multi-currency configuration. --- .../2017_06_02_105232_changes_for_v450.php | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 database/migrations/2017_06_02_105232_changes_for_v450.php diff --git a/database/migrations/2017_06_02_105232_changes_for_v450.php b/database/migrations/2017_06_02_105232_changes_for_v450.php new file mode 100644 index 0000000000..f0f39c839d --- /dev/null +++ b/database/migrations/2017_06_02_105232_changes_for_v450.php @@ -0,0 +1,42 @@ +decimal('foreign_amount', 22, 12)->after('amount'); + } + ); + + // add foreign transaction currency id to transactions (is nullable): + Schema::table( + 'transactions', function (Blueprint $table) { + $table->integer('foreign_currency_id', false, true)->after('foreign_amount')->nullable(); + $table->foreign('foreign_currency_id')->references('id')->on('transaction_currencies')->onDelete('set null'); + } + ); + } +} From 0868aac750cd7919c971df386be60675f8993bae Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 4 Jun 2017 08:48:40 +0200 Subject: [PATCH 012/126] Small update for 4.5.0 SQL update. --- database/migrations/2017_06_02_105232_changes_for_v450.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/database/migrations/2017_06_02_105232_changes_for_v450.php b/database/migrations/2017_06_02_105232_changes_for_v450.php index f0f39c839d..1f44bedf52 100644 --- a/database/migrations/2017_06_02_105232_changes_for_v450.php +++ b/database/migrations/2017_06_02_105232_changes_for_v450.php @@ -27,14 +27,14 @@ class ChangesForV450 extends Migration // add "foreign_amount" to transactions Schema::table( 'transactions', function (Blueprint $table) { - $table->decimal('foreign_amount', 22, 12)->after('amount'); + $table->decimal('foreign_amount', 22, 12)->nullable()->after('amount'); } ); // add foreign transaction currency id to transactions (is nullable): Schema::table( 'transactions', function (Blueprint $table) { - $table->integer('foreign_currency_id', false, true)->after('foreign_amount')->nullable(); + $table->integer('foreign_currency_id', false, true)->default(null)->after('foreign_amount')->nullable(); $table->foreign('foreign_currency_id')->references('id')->on('transaction_currencies')->onDelete('set null'); } ); From d37b46effc500f469ea3b2954dac3b2913586361 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 4 Jun 2017 08:48:54 +0200 Subject: [PATCH 013/126] Database upgrade routine. --- app/Console/Commands/UpgradeDatabase.php | 52 ++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 3 deletions(-) diff --git a/app/Console/Commands/UpgradeDatabase.php b/app/Console/Commands/UpgradeDatabase.php index aa2656b712..2c3a20f3a0 100644 --- a/app/Console/Commands/UpgradeDatabase.php +++ b/app/Console/Commands/UpgradeDatabase.php @@ -32,6 +32,7 @@ use Illuminate\Database\QueryException; use Log; use Preferences; use Schema; +use Steam; /** * Class UpgradeDatabase @@ -72,9 +73,54 @@ class UpgradeDatabase extends Command $this->repairPiggyBanks(); $this->updateAccountCurrencies(); $this->updateJournalCurrencies(); + $this->currencyInfoToTransactions(); $this->info('Firefly III database is up to date.'); } + /** + * Moves the currency id info to the transaction instead of the journal. + */ + private function currencyInfoToTransactions() + { + $count = 0; + $expanded = 0; + $set = TransactionJournal::with('transactions')->get(); + /** @var TransactionJournal $journal */ + foreach ($set as $journal) { + /** @var Transaction $transaction */ + foreach ($journal->transactions as $transaction) { + if (is_null($transaction->transaction_currency_id)) { + $transaction->transaction_currency_id = $journal->transaction_currency_id; + $transaction->save(); + $count++; + } + } + + + // read and use the foreign amounts when present. + if ($journal->hasMeta('foreign_amount')) { + $amount = Steam::positive($journal->getMeta('foreign_amount')); + + // update both transactions: + foreach ($journal->transactions as $transaction) { + $transaction->foreign_amount = $amount; + if (bccomp($transaction->amount, '0') === -1) { + // update with negative amount: + $transaction->foreign_amount = bcmul($amount, '-1'); + } + // set foreign currency id: + $transaction->foreign_currency_id = intval($journal->getMeta('foreign_currency_id')); + $transaction->save(); + } + $journal->deleteMeta('foreign_amount'); + $journal->deleteMeta('foreign_currency_id'); + } + + } + + $this->line(sprintf('Updated currency information for %d transactions', $count)); + } + /** * Migrate budget repetitions to new format. */ @@ -269,7 +315,7 @@ class UpgradeDatabase extends Command $repository = app(CurrencyRepositoryInterface::class); $notification = '%s #%d uses %s but should use %s. It has been updated. Please verify this in Firefly III.'; $transfer = 'Transfer #%d has been updated to use the correct currencies. Please verify this in Firefly III.'; - $driver = DB::connection()->getDriverName(); + $driver = DB::connection()->getDriverName(); foreach ($types as $type => $operator) { $query = TransactionJournal @@ -282,10 +328,10 @@ class UpgradeDatabase extends Command ->leftJoin('account_meta', 'account_meta.account_id', '=', 'accounts.id') ->where('transaction_types.type', $type) ->where('account_meta.name', 'currency_id'); - if($driver === 'postgresql') { + if ($driver === 'postgresql') { $query->where('transaction_journals.transaction_currency_id', '!=', DB::raw('cast(account_meta.data as int)')); } - if($driver !== 'postgresql') { + if ($driver !== 'postgresql') { $query->where('transaction_journals.transaction_currency_id', '!=', DB::raw('account_meta.data')); } From 4ce4c3138c3d36164b3e6a7b887d57cd31b1fd88 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 4 Jun 2017 08:49:22 +0200 Subject: [PATCH 014/126] Update export routine so currency information is taken from the transaction. --- app/Export/Collector/JournalExportCollector.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Export/Collector/JournalExportCollector.php b/app/Export/Collector/JournalExportCollector.php index 175b405f79..b0e04e115e 100644 --- a/app/Export/Collector/JournalExportCollector.php +++ b/app/Export/Collector/JournalExportCollector.php @@ -303,7 +303,7 @@ class JournalExportCollector extends BasicCollector implements CollectorInterfac ->leftJoin('accounts', 'transactions.account_id', '=', 'accounts.id') ->leftJoin('accounts AS opposing_accounts', 'opposing.account_id', '=', 'opposing_accounts.id') ->leftJoin('transaction_types', 'transaction_journals.transaction_type_id', 'transaction_types.id') - ->leftJoin('transaction_currencies', 'transaction_journals.transaction_currency_id', '=', 'transaction_currencies.id') + ->leftJoin('transaction_currencies', 'transactions.transaction_currency_id', '=', 'transaction_currencies.id') ->whereIn('transactions.account_id', $accountIds) ->where('transaction_journals.user_id', $this->job->user_id) ->where('transaction_journals.date', '>=', $this->start->format('Y-m-d')) @@ -338,7 +338,7 @@ class JournalExportCollector extends BasicCollector implements CollectorInterfac 'transaction_journals.encrypted as journal_encrypted', 'transaction_journals.transaction_type_id', 'transaction_types.type as transaction_type', - 'transaction_journals.transaction_currency_id', + 'transactions.transaction_currency_id', 'transaction_currencies.code AS transaction_currency_code', ] From 771ebde295a23548321a4ba98059602e4159886f Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 4 Jun 2017 08:49:37 +0200 Subject: [PATCH 015/126] Update journal collector so currency information is taken from the transaction. --- app/Helpers/Collector/JournalCollector.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/Helpers/Collector/JournalCollector.php b/app/Helpers/Collector/JournalCollector.php index 56d53d7446..a85535d907 100644 --- a/app/Helpers/Collector/JournalCollector.php +++ b/app/Helpers/Collector/JournalCollector.php @@ -60,7 +60,6 @@ class JournalCollector implements JournalCollectorInterface 'transaction_journals.description', 'transaction_journals.date', 'transaction_journals.encrypted', - 'transaction_currencies.code as transaction_currency_code', 'transaction_types.type as transaction_type_type', 'transaction_journals.bill_id', 'bills.name as bill_name', @@ -71,6 +70,7 @@ class JournalCollector implements JournalCollectorInterface 'transactions.account_id', 'transactions.identifier', 'transactions.transaction_journal_id', + 'transaction_currencies.code as transaction_currency_code', 'accounts.name as account_name', 'accounts.encrypted as account_encrypted', 'account_types.type as account_type', @@ -484,11 +484,11 @@ class JournalCollector implements JournalCollectorInterface Log::debug('journalCollector::startQuery'); /** @var EloquentBuilder $query */ $query = Transaction::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id') - ->leftJoin('transaction_currencies', 'transaction_currencies.id', 'transaction_journals.transaction_currency_id') ->leftJoin('transaction_types', 'transaction_types.id', 'transaction_journals.transaction_type_id') ->leftJoin('bills', 'bills.id', 'transaction_journals.bill_id') ->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id') ->leftJoin('account_types', 'accounts.account_type_id', 'account_types.id') + ->leftJoin('transaction_currencies', 'transaction_currencies.id', 'transactions.transaction_currency_id') ->whereNull('transactions.deleted_at') ->whereNull('transaction_journals.deleted_at') ->where('transaction_journals.user_id', $this->user->id) From 82e74a2afd98af278bf82fa31ef81339623cc27a Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 4 Jun 2017 13:39:16 +0200 Subject: [PATCH 016/126] Big update to properly support multi currencies. --- .../Transaction/MassController.php | 42 +++- .../Transaction/SingleController.php | 40 ++-- .../Transaction/SplitController.php | 33 +-- .../Controllers/TransactionController.php | 11 +- app/Import/ImportStorage.php | 1 + app/Import/ImportValidator.php | 2 + app/Models/Transaction.php | 33 ++- app/Models/TransactionJournal.php | 41 ---- app/Providers/FireflyServiceProvider.php | 3 +- .../Account/AccountRepository.php | 2 + .../Journal/JournalRepository.php | 192 +++++++++++------- app/Repositories/Journal/JournalTasker.php | 53 +++-- app/Support/Amount.php | 55 +---- app/Support/Binder/JournalList.php | 9 +- .../Models/TransactionJournalTrait.php | 8 + app/Support/Twig/Account.php | 63 ------ app/Support/Twig/AmountFormat.php | 109 ++++++++++ app/Support/Twig/General.php | 41 ---- app/Support/Twig/Transaction.php | 75 ------- database/factories/ModelFactory.php | 1 + public/js/ff/transactions/single/edit.js | 5 +- public/js/ff/transactions/split/edit.js | 20 ++ resources/lang/en_US/firefly.php | 1 + resources/views/list/journals-tiny.twig | 6 +- resources/views/list/journals.twig | 11 +- resources/views/popup/list/journals.twig | 6 +- .../reports/partials/journals-audit.twig | 6 +- .../search/partials/transactions-large.twig | 13 +- .../views/search/partials/transactions.twig | 6 +- resources/views/transactions/mass-delete.twig | 3 +- resources/views/transactions/mass/edit.twig | 13 +- resources/views/transactions/show.twig | 30 ++- resources/views/transactions/single/edit.twig | 4 +- resources/views/transactions/split/edit.twig | 30 ++- 34 files changed, 470 insertions(+), 498 deletions(-) delete mode 100644 app/Support/Twig/Account.php create mode 100644 app/Support/Twig/AmountFormat.php diff --git a/app/Http/Controllers/Transaction/MassController.php b/app/Http/Controllers/Transaction/MassController.php index 285a73f3e9..b0205f36af 100644 --- a/app/Http/Controllers/Transaction/MassController.php +++ b/app/Http/Controllers/Transaction/MassController.php @@ -19,6 +19,7 @@ use FireflyIII\Http\Requests\MassDeleteJournalRequest; use FireflyIII\Http\Requests\MassEditJournalRequest; use FireflyIII\Models\AccountType; use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; @@ -126,8 +127,7 @@ class MassController extends Controller $budgetRepository = app(BudgetRepositoryInterface::class); $budgets = $budgetRepository->getBudgets(); - // skip transactions that have multiple destinations - // or multiple sources: + // skip transactions that have multiple destinations, multiple sources or are an opening balance. $filtered = new Collection; $messages = []; /** @@ -146,6 +146,10 @@ class MassController extends Controller $messages[] = trans('firefly.cannot_edit_multiple_dest', ['description' => $journal->description, 'id' => $journal->id]); continue; } + if ($journal->transactionType->type === TransactionType::OPENING_BALANCE) { + $messages[] = trans('firefly.cannot_edit_opening_balance'); + continue; + } $filtered->push($journal); } @@ -158,13 +162,21 @@ class MassController extends Controller Session::flash('gaEventCategory', 'transactions'); Session::flash('gaEventAction', 'mass-edit'); - // set some values to be used in the edit routine: + // collect some useful meta data for the mass edit: $filtered->each( function (TransactionJournal $journal) { - $journal->amount = $journal->amountPositive(); - $sources = $journal->sourceAccountList(); - $destinations = $journal->destinationAccountList(); - $journal->transaction_count = $journal->transactions()->count(); + $transaction = $journal->positiveTransaction(); + $currency = $transaction->transactionCurrency; + $journal->amount = floatval($transaction->amount); + $sources = $journal->sourceAccountList(); + $destinations = $journal->destinationAccountList(); + $journal->transaction_count = $journal->transactions()->count(); + $journal->currency_symbol = $currency->symbol; + $journal->transaction_type_type = $journal->transactionType->type; + + $journal->foreign_amount = floatval($transaction->foreign_amount); + $journal->foreign_currency = $transaction->foreignCurrency; + if (!is_null($sources->first())) { $journal->source_account_id = $sources->first()->id; $journal->source_account_name = $sources->first()->editname; @@ -195,6 +207,7 @@ class MassController extends Controller { $journalIds = $request->get('journals'); $count = 0; + if (is_array($journalIds)) { foreach ($journalIds as $journalId) { $journal = $repository->find(intval($journalId)); @@ -208,6 +221,10 @@ class MassController extends Controller $budgetId = $request->get('budget_id')[$journal->id] ?? 0; $category = $request->get('category')[$journal->id]; $tags = $journal->tags->pluck('tag')->toArray(); + $amount = round($request->get('amount')[$journal->id], 12); + $foreignAmount = isset($request->get('foreign_amount')[$journal->id]) ? round($request->get('foreign_amount')[$journal->id], 12) : null; + $foreignCurrencyId = isset($request->get('foreign_currency_id')[$journal->id]) ? + intval($request->get('foreign_currency_id')[$journal->id]) : null; // build data array $data = [ @@ -218,16 +235,20 @@ class MassController extends Controller 'source_account_name' => $sourceAccountName, 'destination_account_id' => intval($destAccountId), 'destination_account_name' => $destAccountName, - 'amount' => round($request->get('amount')[$journal->id], 12), - 'currency_id' => $journal->transaction_currency_id, + 'amount' => $foreignAmount, + 'native_amount' => $amount, + 'source_amount' => $amount, 'date' => new Carbon($request->get('date')[$journal->id]), 'interest_date' => $journal->interest_date, 'book_date' => $journal->book_date, 'process_date' => $journal->process_date, 'budget_id' => intval($budgetId), + 'currency_id' => $foreignCurrencyId, + 'foreign_amount' => $foreignAmount, + 'destination_amount' => $foreignAmount, + //'foreign_currency_id' => $foreignCurrencyId, 'category' => $category, 'tags' => $tags, - ]; // call repository update function. $repository->update($journal, $data); @@ -235,6 +256,7 @@ class MassController extends Controller $count++; } } + } Preferences::mark(); Session::flash('success', trans('firefly.mass_edited_transactions_success', ['amount' => $count])); diff --git a/app/Http/Controllers/Transaction/SingleController.php b/app/Http/Controllers/Transaction/SingleController.php index eef9bd7ab4..89de28d7d2 100644 --- a/app/Http/Controllers/Transaction/SingleController.php +++ b/app/Http/Controllers/Transaction/SingleController.php @@ -238,6 +238,7 @@ class SingleController extends Controller $sourceAccounts = $journal->sourceAccountList(); $destinationAccounts = $journal->destinationAccountList(); $optionalFields = Preferences::get('transaction_journal_optional_fields', [])->data; + $pTransaction = $journal->positiveTransaction(); $preFilled = [ 'date' => $journal->dateAsString(), 'interest_date' => $journal->dateAsString('interest_date'), @@ -250,8 +251,6 @@ class SingleController extends Controller 'source_account_name' => $sourceAccounts->first()->edit_name, 'destination_account_id' => $destinationAccounts->first()->id, 'destination_account_name' => $destinationAccounts->first()->edit_name, - 'amount' => $journal->amountPositive(), - 'currency' => $journal->transactionCurrency, // new custom fields: 'due_date' => $journal->dateAsString('due_date'), @@ -260,26 +259,36 @@ class SingleController extends Controller 'interal_reference' => $journal->getMeta('internal_reference'), 'notes' => $journal->getMeta('notes'), - // exchange rate fields - 'native_amount' => $journal->amountPositive(), - 'native_currency' => $journal->transactionCurrency, + // amount fields + 'amount' => $pTransaction->amount, + 'source_amount' => $pTransaction->amount, + 'native_amount' => $pTransaction->amount, + 'destination_amount' => $pTransaction->foreign_amount, + 'currency' => $pTransaction->transactionCurrency, + 'source_currency' => $pTransaction->transactionCurrency, + 'native_currency' => $pTransaction->transactionCurrency, + 'foreign_currency' => !is_null($pTransaction->foreignCurrency) ? $pTransaction->foreignCurrency : $pTransaction->transactionCurrency, + 'destination_currency' => !is_null($pTransaction->foreignCurrency) ? $pTransaction->foreignCurrency : $pTransaction->transactionCurrency, ]; - // if user has entered a foreign currency, update some fields - $foreignCurrencyId = intval($journal->getMeta('foreign_currency_id')); - if ($foreignCurrencyId > 0) { - // update some fields in pre-filled. - // @codeCoverageIgnoreStart - $preFilled['amount'] = $journal->getMeta('foreign_amount'); - $preFilled['currency'] = $this->currency->find(intval($journal->getMeta('foreign_currency_id'))); - // @codeCoverageIgnoreEnd + // amounts for withdrawals and deposits: + // (amount, native_amount, source_amount, destination_amount) + if (($journal->isWithdrawal() || $journal->isDeposit()) && !is_null($pTransaction->foreign_amount)) { + $preFilled['amount'] = $pTransaction->foreign_amount; + $preFilled['currency'] = $pTransaction->foreignCurrency; } - if ($journal->isWithdrawal() && $destinationAccounts->first()->accountType->type == AccountType::CASH) { + if ($journal->isTransfer() && !is_null($pTransaction->foreign_amount)) { + $preFilled['destination_amount'] = $pTransaction->foreign_amount; + $preFilled['destination_currency'] = $pTransaction->foreignCurrency; + } + + // fixes for cash accounts: + if ($journal->isWithdrawal() && $destinationAccounts->first()->accountType->type === AccountType::CASH) { $preFilled['destination_account_name'] = ''; } - if ($journal->isDeposit() && $sourceAccounts->first()->accountType->type == AccountType::CASH) { + if ($journal->isDeposit() && $sourceAccounts->first()->accountType->type === AccountType::CASH) { $preFilled['source_account_name'] = ''; } @@ -319,6 +328,7 @@ class SingleController extends Controller return redirect(route('transactions.create', [$request->input('what')]))->withInput(); } + /** @var array $files */ $files = $request->hasFile('attachments') ? $request->file('attachments') : null; $this->attachments->saveAttachmentsForModel($journal, $files); diff --git a/app/Http/Controllers/Transaction/SplitController.php b/app/Http/Controllers/Transaction/SplitController.php index c587129811..e6668bc43e 100644 --- a/app/Http/Controllers/Transaction/SplitController.php +++ b/app/Http/Controllers/Transaction/SplitController.php @@ -93,7 +93,7 @@ class SplitController extends Controller } $uploadSize = min(Steam::phpBytes(ini_get('upload_max_filesize')), Steam::phpBytes(ini_get('post_max_size'))); - $currencies = ExpandedForm::makeSelectList($this->currencies->get()); + $currencies = $this->currencies->get(); $assetAccounts = ExpandedForm::makeSelectList($this->accounts->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET])); $optionalFields = Preferences::get('transaction_journal_optional_fields', [])->data; $budgets = ExpandedForm::makeSelectListWithEmpty($this->budgets->getActiveBudgets()); @@ -130,7 +130,6 @@ class SplitController extends Controller */ public function update(Request $request, JournalRepositoryInterface $repository, TransactionJournal $journal) { - if ($this->isOpeningBalance($journal)) { return $this->redirectToAccount($journal); } @@ -179,7 +178,6 @@ class SplitController extends Controller 'journal_source_account_id' => $request->get('journal_source_account_id'), 'journal_source_account_name' => $request->get('journal_source_account_name'), 'journal_destination_account_id' => $request->get('journal_destination_account_id'), - 'currency_id' => $request->get('currency_id'), 'what' => $request->get('what'), 'date' => $request->get('date'), // all custom fields: @@ -218,7 +216,6 @@ class SplitController extends Controller 'journal_source_account_id' => $request->old('journal_source_account_id', $sourceAccounts->first()->id), 'journal_source_account_name' => $request->old('journal_source_account_name', $sourceAccounts->first()->name), 'journal_destination_account_id' => $request->old('journal_destination_account_id', $destinationAccounts->first()->id), - 'currency_id' => $request->old('currency_id', $journal->transaction_currency_id), 'destinationAccounts' => $destinationAccounts, 'what' => strtolower($journal->transactionTypeStr()), 'date' => $request->old('date', $journal->date), @@ -253,14 +250,22 @@ class SplitController extends Controller /** @var array $transaction */ foreach ($transactions as $index => $transaction) { $set = [ - 'description' => $transaction['description'], - 'source_account_id' => $transaction['source_account_id'], - 'source_account_name' => $transaction['source_account_name'], - 'destination_account_id' => $transaction['destination_account_id'], - 'destination_account_name' => $transaction['destination_account_name'], - 'amount' => round($transaction['destination_amount'], 12), - 'budget_id' => isset($transaction['budget_id']) ? intval($transaction['budget_id']) : 0, - 'category' => $transaction['category'], + 'description' => $transaction['description'], + 'source_account_id' => $transaction['source_account_id'], + 'source_account_name' => $transaction['source_account_name'], + 'destination_account_id' => $transaction['destination_account_id'], + 'destination_account_name' => $transaction['destination_account_name'], + 'amount' => round($transaction['destination_amount'], 12), + 'budget_id' => isset($transaction['budget_id']) ? intval($transaction['budget_id']) : 0, + 'category' => $transaction['category'], + 'transaction_currency_id' => $transaction['transaction_currency_id'], + 'transaction_currency_code' => $transaction['transaction_currency_code'], + 'transaction_currency_symbol' => $transaction['transaction_currency_symbol'], + 'foreign_amount' => round($transaction['foreign_destination_amount'], 12), + 'foreign_currency_id' => $transaction['foreign_currency_id'], + 'foreign_currency_code' => $transaction['foreign_currency_code'], + 'foreign_currency_symbol' => $transaction['foreign_currency_symbol'], + ]; // set initial category and/or budget: @@ -294,8 +299,12 @@ class SplitController extends Controller 'destination_account_id' => $transaction['destination_account_id'] ?? 0, 'destination_account_name' => $transaction['destination_account_name'] ?? '', 'amount' => round($transaction['amount'] ?? 0, 12), + 'foreign_amount' => !isset($transaction['foreign_amount']) ? null : round($transaction['foreign_amount'] ?? 0, 12), 'budget_id' => isset($transaction['budget_id']) ? intval($transaction['budget_id']) : 0, 'category' => $transaction['category'] ?? '', + 'transaction_currency_id' => intval($transaction['transaction_currency_id']), + 'foreign_currency_id' => $transaction['foreign_currency_id'] ?? null, + ]; } Log::debug(sprintf('Found %d splits in request data.', count($return))); diff --git a/app/Http/Controllers/TransactionController.php b/app/Http/Controllers/TransactionController.php index 1e838c5dec..5f6f12de2e 100644 --- a/app/Http/Controllers/TransactionController.php +++ b/app/Http/Controllers/TransactionController.php @@ -183,17 +183,8 @@ class TransactionController extends Controller $transactions = $tasker->getTransactionsOverview($journal); $what = strtolower($journal->transaction_type_type ?? $journal->transactionType->type); $subTitle = trans('firefly.' . $what) . ' "' . e($journal->description) . '"'; - $foreignCurrency = null; - if ($journal->hasMeta('foreign_currency_id')) { - // @codeCoverageIgnoreStart - /** @var CurrencyRepositoryInterface $repository */ - $repository = app(CurrencyRepositoryInterface::class); - $foreignCurrency = $repository->find(intval($journal->getMeta('foreign_currency_id'))); - // @codeCoverageIgnoreEnd - } - - return view('transactions.show', compact('journal', 'events', 'subTitle', 'what', 'transactions', 'foreignCurrency')); + return view('transactions.show', compact('journal', 'events', 'subTitle', 'what', 'transactions')); } diff --git a/app/Import/ImportStorage.php b/app/Import/ImportStorage.php index 2d5524fcec..4f39086965 100644 --- a/app/Import/ImportStorage.php +++ b/app/Import/ImportStorage.php @@ -291,6 +291,7 @@ class ImportStorage 'user_id' => $entry->user->id, 'transaction_type_id' => $entry->fields['transaction-type']->id, 'bill_id' => $billId, + // TODO update this transaction currency reference. 'transaction_currency_id' => $entry->fields['currency']->id, 'description' => $entry->fields['description'], 'date' => $entry->fields['date-transaction'], diff --git a/app/Import/ImportValidator.php b/app/Import/ImportValidator.php index 3a3a373b2b..fd9981ec47 100644 --- a/app/Import/ImportValidator.php +++ b/app/Import/ImportValidator.php @@ -74,6 +74,7 @@ class ImportValidator $entry = $this->setOpposingAccount($entry); $entry = $this->cleanDescription($entry); $entry = $this->setTransactionType($entry); + // TODO update this transaction currency reference. $entry = $this->setTransactionCurrency($entry); $newCollection->put($index, $entry); @@ -383,6 +384,7 @@ class ImportValidator */ private function setTransactionCurrency(ImportEntry $entry): ImportEntry { + // TODO update this transaction currency reference. if (is_null($entry->fields['currency'])) { /** @var CurrencyRepositoryInterface $repository */ $repository = app(CurrencyRepositoryInterface::class); diff --git a/app/Models/Transaction.php b/app/Models/Transaction.php index ef5deeadcd..0e9ade0b30 100644 --- a/app/Models/Transaction.php +++ b/app/Models/Transaction.php @@ -26,7 +26,6 @@ use Watson\Validating\ValidatingTrait; */ class Transaction extends Model { - /** * The attributes that should be casted to native types. * @@ -42,16 +41,18 @@ class Transaction extends Model 'bill_name_encrypted' => 'boolean', ]; protected $dates = ['created_at', 'updated_at', 'deleted_at']; - protected $fillable = ['account_id', 'transaction_journal_id', 'description', 'amount', 'identifier']; + protected $fillable = ['account_id', 'transaction_journal_id', 'description', 'amount', 'identifier', 'transaction_currency_id', 'foreign_currency_id','foreign_amount']; protected $hidden = ['encrypted']; protected $rules = [ - 'account_id' => 'required|exists:accounts,id', - 'transaction_journal_id' => 'required|exists:transaction_journals,id', - 'description' => 'between:0,1024', - 'amount' => 'required|numeric', + 'account_id' => 'required|exists:accounts,id', + 'transaction_journal_id' => 'required|exists:transaction_journals,id', + 'transaction_currency_id' => 'required|exists:transaction_currencies,id', + //'foreign_currency_id' => 'exists:transaction_currencies,id', + 'description' => 'between:0,1024', + 'amount' => 'required|numeric', + //'foreign_amount' => 'numeric', ]; - use SoftDeletes, ValidatingTrait; /** * @param Builder $query @@ -74,6 +75,8 @@ class Transaction extends Model return false; } + use SoftDeletes, ValidatingTrait; + /** * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ @@ -160,6 +163,22 @@ class Transaction extends Model $this->attributes['amount'] = strval(round($value, 12)); } + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function transactionCurrency() + { + return $this->belongsTo('FireflyIII\Models\TransactionCurrency'); + } + + /** + * @return \Illuminate\Database\Eloquent\Relations\BelongsTo + */ + public function foreignCurrency() + { + return $this->belongsTo('FireflyIII\Models\TransactionCurrency','foreign_currency_id'); + } + /** * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ diff --git a/app/Models/TransactionJournal.php b/app/Models/TransactionJournal.php index 53506bc61d..cc0c00ef45 100644 --- a/app/Models/TransactionJournal.php +++ b/app/Models/TransactionJournal.php @@ -68,7 +68,6 @@ class TransactionJournal extends Model = [ 'user_id' => 'required|exists:users,id', 'transaction_type_id' => 'required|exists:transaction_types,id', - 'transaction_currency_id' => 'required|exists:transaction_currencies,id', 'description' => 'required|between:1,1024', 'completed' => 'required|boolean', 'date' => 'required|date', @@ -299,46 +298,6 @@ class TransactionJournal extends Model return $query->where('transaction_journals.date', '<=', $date->format('Y-m-d 00:00:00')); } - /** - * @param EloquentBuilder $query - */ - public function scopeExpanded(EloquentBuilder $query) - { - // left join transaction type: - if (!self::isJoined($query, 'transaction_types')) { - $query->leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id'); - } - - // left join transaction currency: - $query->leftJoin('transaction_currencies', 'transaction_currencies.id', '=', 'transaction_journals.transaction_currency_id'); - - // extend group by: - $query->groupBy( - [ - 'transaction_journals.id', - 'transaction_journals.created_at', - 'transaction_journals.updated_at', - 'transaction_journals.deleted_at', - 'transaction_journals.user_id', - 'transaction_journals.transaction_type_id', - 'transaction_journals.bill_id', - 'transaction_journals.transaction_currency_id', - 'transaction_journals.description', - 'transaction_journals.date', - 'transaction_journals.interest_date', - 'transaction_journals.book_date', - 'transaction_journals.process_date', - 'transaction_journals.order', - 'transaction_journals.tag_count', - 'transaction_journals.encrypted', - 'transaction_journals.completed', - 'transaction_types.type', - 'transaction_currencies.code', - ] - ); - $query->with(['categories', 'budgets', 'attachments', 'bill', 'transactions']); - } - /** * @param EloquentBuilder $query */ diff --git a/app/Providers/FireflyServiceProvider.php b/app/Providers/FireflyServiceProvider.php index ef4890bb7f..c9740f4bcf 100644 --- a/app/Providers/FireflyServiceProvider.php +++ b/app/Providers/FireflyServiceProvider.php @@ -44,6 +44,7 @@ use FireflyIII\Support\Navigation; use FireflyIII\Support\Preferences; use FireflyIII\Support\Steam; use FireflyIII\Support\Twig\Account; +use FireflyIII\Support\Twig\AmountFormat; use FireflyIII\Support\Twig\General; use FireflyIII\Support\Twig\Journal; use FireflyIII\Support\Twig\PiggyBank; @@ -79,7 +80,7 @@ class FireflyServiceProvider extends ServiceProvider Twig::addExtension(new Translation); Twig::addExtension(new Transaction); Twig::addExtension(new Rule); - Twig::addExtension(new Account); + Twig::addExtension(new AmountFormat); } /** diff --git a/app/Repositories/Account/AccountRepository.php b/app/Repositories/Account/AccountRepository.php index 122e8f7d5c..7b594555b8 100644 --- a/app/Repositories/Account/AccountRepository.php +++ b/app/Repositories/Account/AccountRepository.php @@ -481,6 +481,7 @@ class AccountRepository implements AccountRepositoryInterface [ 'user_id' => $this->user->id, 'transaction_type_id' => $transactionType->id, + // TODO update this transaction currency reference. 'transaction_currency_id' => $currencyId, 'description' => 'Initial balance for "' . $account->name . '"', 'completed' => true, @@ -622,6 +623,7 @@ class AccountRepository implements AccountRepositoryInterface // update date: $journal->date = $date; + // TODO update this transaction currency reference. $journal->transaction_currency_id = $currencyId; $journal->save(); // update transactions: diff --git a/app/Repositories/Journal/JournalRepository.php b/app/Repositories/Journal/JournalRepository.php index ef4820ac2e..9a28be284b 100644 --- a/app/Repositories/Journal/JournalRepository.php +++ b/app/Repositories/Journal/JournalRepository.php @@ -39,7 +39,6 @@ class JournalRepository implements JournalRepositoryInterface { /** @var User */ private $user; - /** @var array */ private $validMetaFields = ['interest_date', 'book_date', 'process_date', 'due_date', 'payment_date', 'invoice_date', 'internal_reference', 'notes', 'foreign_amount', @@ -182,13 +181,12 @@ class JournalRepository implements JournalRepositoryInterface $transactionType = TransactionType::where('type', ucfirst($data['what']))->first(); $accounts = $this->storeAccounts($transactionType, $data); $data = $this->verifyNativeAmount($data, $accounts); - $currencyId = $data['currency_id']; $amount = strval($data['amount']); $journal = new TransactionJournal( [ 'user_id' => $this->user->id, 'transaction_type_id' => $transactionType->id, - 'transaction_currency_id' => $currencyId, + 'transaction_currency_id' => $data['currency_id'], // no longer used. 'description' => $data['description'], 'completed' => 0, 'date' => $data['date'], @@ -200,27 +198,32 @@ class JournalRepository implements JournalRepositoryInterface $this->storeCategoryWithJournal($journal, $data['category']); $this->storeBudgetWithJournal($journal, $data['budget_id']); - // store two transactions: $one = [ - 'journal' => $journal, - 'account' => $accounts['source'], - 'amount' => bcmul($amount, '-1'), - 'description' => null, - 'category' => null, - 'budget' => null, - 'identifier' => 0, + 'journal' => $journal, + 'account' => $accounts['source'], + 'amount' => bcmul($amount, '-1'), + 'transaction_currency_id' => $data['currency_id'], + 'foreign_amount' => is_null($data['foreign_amount']) ? null : bcmul(strval($data['foreign_amount']), '-1'), + 'foreign_currency_id' => $data['foreign_currency_id'], + 'description' => null, + 'category' => null, + 'budget' => null, + 'identifier' => 0, ]; $this->storeTransaction($one); $two = [ - 'journal' => $journal, - 'account' => $accounts['destination'], - 'amount' => $amount, - 'description' => null, - 'category' => null, - 'budget' => null, - 'identifier' => 0, + 'journal' => $journal, + 'account' => $accounts['destination'], + 'amount' => $amount, + 'transaction_currency_id' => $data['currency_id'], + 'foreign_amount' => $data['foreign_amount'], + 'foreign_currency_id' => $data['foreign_currency_id'], + 'description' => null, + 'category' => null, + 'budget' => null, + 'identifier' => 0, ]; $this->storeTransaction($two); @@ -256,11 +259,14 @@ class JournalRepository implements JournalRepositoryInterface { // update actual journal: - $journal->description = $data['description']; - $journal->date = $data['date']; - $accounts = $this->storeAccounts($journal->transactionType, $data); - $data = $this->verifyNativeAmount($data, $accounts); - $amount = strval($data['amount']); + $journal->description = $data['description']; + $journal->date = $data['date']; + $accounts = $this->storeAccounts($journal->transactionType, $data); + $data = $this->verifyNativeAmount($data, $accounts); + $data['amount'] = strval($data['amount']); + $data['foreign_amount'] = is_null($data['foreign_amount']) ? null : strval($data['foreign_amount']); + + var_dump($data); // unlink all categories, recreate them: $journal->categories()->detach(); @@ -269,9 +275,11 @@ class JournalRepository implements JournalRepositoryInterface $this->storeCategoryWithJournal($journal, $data['category']); $this->storeBudgetWithJournal($journal, $data['budget_id']); + // negative because source loses money. + $this->updateSourceTransaction($journal, $accounts['source'], $data); - $this->updateSourceTransaction($journal, $accounts['source'], bcmul($amount, '-1')); // negative because source loses money. - $this->updateDestinationTransaction($journal, $accounts['destination'], $amount); // positive because destination gets money. + // positive because destination gets money. + $this->updateDestinationTransaction($journal, $accounts['destination'], $data); $journal->save(); @@ -308,9 +316,8 @@ class JournalRepository implements JournalRepositoryInterface public function updateSplitJournal(TransactionJournal $journal, array $data): TransactionJournal { // update actual journal: - $journal->transaction_currency_id = $data['currency_id']; - $journal->description = $data['journal_description']; - $journal->date = $data['date']; + $journal->description = $data['journal_description']; + $journal->date = $data['date']; $journal->save(); Log::debug(sprintf('Updated split journal #%d', $journal->id)); @@ -342,6 +349,7 @@ class JournalRepository implements JournalRepositoryInterface // store each transaction. $identifier = 0; Log::debug(sprintf('Count %d transactions in updateSplitJournal()', count($data['transactions']))); + foreach ($data['transactions'] as $transaction) { Log::debug(sprintf('Split journal update split transaction %d', $identifier)); $transaction = $this->appendTransactionData($transaction, $data); @@ -564,30 +572,40 @@ class JournalRepository implements JournalRepositoryInterface $accounts = $this->storeAccounts($journal->transactionType, $transaction); // store transaction one way: - $one = $this->storeTransaction( + $amount = bcmul(strval($transaction['amount']), '-1'); + $foreignAmount = is_null($transaction['foreign_amount']) ? null : bcmul(strval($transaction['foreign_amount']), '-1'); + $one = $this->storeTransaction( [ - 'journal' => $journal, - 'account' => $accounts['source'], - 'amount' => bcmul(strval($transaction['amount']), '-1'), - 'description' => $transaction['description'], - 'category' => null, - 'budget' => null, - 'identifier' => $identifier, + 'journal' => $journal, + 'account' => $accounts['source'], + 'amount' => $amount, + 'transaction_currency_id' => $transaction['transaction_currency_id'], + 'foreign_amount' => $foreignAmount, + 'foreign_currency_id' => $transaction['foreign_currency_id'], + 'description' => $transaction['description'], + 'category' => null, + 'budget' => null, + 'identifier' => $identifier, ] ); $this->storeCategoryWithTransaction($one, $transaction['category']); $this->storeBudgetWithTransaction($one, $transaction['budget_id']); // and the other way: - $two = $this->storeTransaction( + $amount = strval($transaction['amount']); + $foreignAmount = is_null($transaction['foreign_amount']) ? null : strval($transaction['foreign_amount']); + $two = $this->storeTransaction( [ - 'journal' => $journal, - 'account' => $accounts['destination'], - 'amount' => strval($transaction['amount']), - 'description' => $transaction['description'], - 'category' => null, - 'budget' => null, - 'identifier' => $identifier, + 'journal' => $journal, + 'account' => $accounts['destination'], + 'amount' => $amount, + 'transaction_currency_id' => $transaction['transaction_currency_id'], + 'foreign_amount' => $foreignAmount, + 'foreign_currency_id' => $transaction['foreign_currency_id'], + 'description' => $transaction['description'], + 'category' => null, + 'budget' => null, + 'identifier' => $identifier, ] ); $this->storeCategoryWithTransaction($two, $transaction['category']); @@ -603,16 +621,27 @@ class JournalRepository implements JournalRepositoryInterface */ private function storeTransaction(array $data): Transaction { + $fields = [ + 'transaction_journal_id' => $data['journal']->id, + 'account_id' => $data['account']->id, + 'amount' => $data['amount'], + 'foreign_amount' => $data['foreign_amount'], + 'transaction_currency_id' => $data['transaction_currency_id'], + 'foreign_currency_id' => $data['foreign_currency_id'], + 'description' => $data['description'], + 'identifier' => $data['identifier'], + ]; + + + if (is_null($data['foreign_currency_id'])) { + unset($fields['foreign_currency_id']); + } + if (is_null($data['foreign_amount'])) { + unset($fields['foreign_amount']); + } + /** @var Transaction $transaction */ - $transaction = Transaction::create( - [ - 'transaction_journal_id' => $data['journal']->id, - 'account_id' => $data['account']->id, - 'amount' => $data['amount'], - 'description' => $data['description'], - 'identifier' => $data['identifier'], - ] - ); + $transaction = Transaction::create($fields); Log::debug(sprintf('Transaction stored with ID: %s', $transaction->id)); @@ -675,22 +704,23 @@ class JournalRepository implements JournalRepositoryInterface /** * @param TransactionJournal $journal * @param Account $account - * @param string $amount + * @param array $data * * @throws FireflyException */ - private function updateDestinationTransaction(TransactionJournal $journal, Account $account, string $amount) + private function updateDestinationTransaction(TransactionJournal $journal, Account $account, array $data) { - // should be one: $set = $journal->transactions()->where('amount', '>', 0)->get(); if ($set->count() != 1) { - throw new FireflyException( - sprintf('Journal #%d has an unexpected (%d) amount of transactions with an amount more than zero.', $journal->id, $set->count()) - ); + throw new FireflyException(sprintf('Journal #%d has %d transactions with an amount more than zero.', $journal->id, $set->count())); } /** @var Transaction $transaction */ - $transaction = $set->first(); - $transaction->amount = $amount; + $transaction = $set->first(); + $transaction->amount = app('steam')->positive($data['amount']); + $transaction->transaction_currency_id = $data['currency_id']; + $transaction->foreign_amount = is_null($data['foreign_amount']) ? null : app('steam')->positive($data['foreign_amount']); + $transaction->foreign_currency_id = $data['foreign_currency_id']; + $transaction->account_id = $account->id; $transaction->save(); @@ -699,26 +729,24 @@ class JournalRepository implements JournalRepositoryInterface /** * @param TransactionJournal $journal * @param Account $account - * @param string $amount + * @param array $data * * @throws FireflyException */ - private function updateSourceTransaction(TransactionJournal $journal, Account $account, string $amount) + private function updateSourceTransaction(TransactionJournal $journal, Account $account, array $data) { // should be one: $set = $journal->transactions()->where('amount', '<', 0)->get(); if ($set->count() != 1) { - throw new FireflyException( - sprintf('Journal #%d has an unexpected (%d) amount of transactions with an amount less than zero.', $journal->id, $set->count()) - ); + throw new FireflyException(sprintf('Journal #%d has %d transactions with an amount more than zero.', $journal->id, $set->count())); } /** @var Transaction $transaction */ - $transaction = $set->first(); - $transaction->amount = $amount; - $transaction->account_id = $account->id; + $transaction = $set->first(); + $transaction->amount = bcmul(app('steam')->positive($data['amount']), '-1'); + $transaction->transaction_currency_id = $data['currency_id']; + $transaction->foreign_amount = is_null($data['foreign_amount']) ? null : bcmul(app('steam')->positive($data['foreign_amount']), '-1'); + $transaction->foreign_currency_id = $data['foreign_currency_id']; $transaction->save(); - - } /** @@ -777,8 +805,10 @@ class JournalRepository implements JournalRepositoryInterface private function verifyNativeAmount(array $data, array $accounts): array { /** @var TransactionType $transactionType */ - $transactionType = TransactionType::where('type', ucfirst($data['what']))->first(); - $submittedCurrencyId = $data['currency_id']; + $transactionType = TransactionType::where('type', ucfirst($data['what']))->first(); + $submittedCurrencyId = $data['currency_id']; + $data['foreign_amount'] = null; + $data['foreign_currency_id'] = null; // which account to check for what the native currency is? $check = 'source'; @@ -803,11 +833,17 @@ class JournalRepository implements JournalRepositoryInterface } break; case TransactionType::TRANSFER: - // source gets the original amount. - $data['amount'] = strval($data['source_amount']); - $data['currency_id'] = intval($accounts['source']->getMeta('currency_id')); - $data['foreign_amount'] = strval($data['destination_amount']); - $data['foreign_currency_id'] = intval($accounts['destination']->getMeta('currency_id')); + $sourceCurrencyId = intval($accounts['source']->getMeta('currency_id')); + $destinationCurrencyId = intval($accounts['destination']->getMeta('currency_id')); + $data['amount'] = strval($data['source_amount']); + $data['currency_id'] = intval($accounts['source']->getMeta('currency_id')); + + if ($sourceCurrencyId !== $destinationCurrencyId) { + // accounts have different id's, save this info: + $data['foreign_amount'] = strval($data['destination_amount']); + $data['foreign_currency_id'] = $destinationCurrencyId; + } + break; default: throw new FireflyException(sprintf('Cannot handle %s in verifyNativeAmount()', $transactionType->type)); diff --git a/app/Repositories/Journal/JournalTasker.php b/app/Repositories/Journal/JournalTasker.php index b56abe0db8..bb0c8bcdd2 100644 --- a/app/Repositories/Journal/JournalTasker.php +++ b/app/Repositories/Journal/JournalTasker.php @@ -81,6 +81,8 @@ class JournalTasker implements JournalTaskerInterface ->leftJoin('account_types as source_account_types', 'source_accounts.account_type_id', '=', 'source_account_types.id') ->leftJoin('accounts as destination_accounts', 'destination.account_id', '=', 'destination_accounts.id') ->leftJoin('account_types as destination_account_types', 'destination_accounts.account_type_id', '=', 'destination_account_types.id') + ->leftJoin('transaction_currencies as native_currencies', 'transactions.transaction_currency_id', '=', 'native_currencies.id') + ->leftJoin('transaction_currencies as foreign_currencies', 'transactions.foreign_currency_id', '=', 'foreign_currencies.id') ->where('transactions.amount', '<', 0) ->whereNull('transactions.deleted_at') ->get( @@ -91,12 +93,21 @@ class JournalTasker implements JournalTaskerInterface 'source_accounts.encrypted as account_encrypted', 'source_account_types.type as account_type', 'transactions.amount', + 'transactions.foreign_amount', 'transactions.description', 'destination.id as destination_id', 'destination.account_id as destination_account_id', 'destination_accounts.name as destination_account_name', 'destination_accounts.encrypted as destination_account_encrypted', 'destination_account_types.type as destination_account_type', + 'native_currencies.id as transaction_currency_id', + 'native_currencies.code as transaction_currency_code', + 'native_currencies.symbol as transaction_currency_symbol', + + 'foreign_currencies.id as foreign_currency_id', + 'foreign_currencies.code as foreign_currency_code', + 'foreign_currencies.symbol as foreign_currency_symbol', + ] ); @@ -109,23 +120,31 @@ class JournalTasker implements JournalTaskerInterface $budget = $entry->budgets->first(); $category = $entry->categories->first(); $transaction = [ - 'source_id' => $entry->id, - 'source_amount' => $entry->amount, - 'description' => $entry->description, - 'source_account_id' => $entry->account_id, - 'source_account_name' => Steam::decrypt(intval($entry->account_encrypted), $entry->account_name), - 'source_account_type' => $entry->account_type, - 'source_account_before' => $sourceBalance, - 'source_account_after' => bcadd($sourceBalance, $entry->amount), - 'destination_id' => $entry->destination_id, - 'destination_amount' => bcmul($entry->amount, '-1'), - 'destination_account_id' => $entry->destination_account_id, - 'destination_account_type' => $entry->destination_account_type, - 'destination_account_name' => Steam::decrypt(intval($entry->destination_account_encrypted), $entry->destination_account_name), - 'destination_account_before' => $destinationBalance, - 'destination_account_after' => bcadd($destinationBalance, bcmul($entry->amount, '-1')), - 'budget_id' => is_null($budget) ? 0 : $budget->id, - 'category' => is_null($category) ? '' : $category->name, + 'source_id' => $entry->id, + 'source_amount' => $entry->amount, + 'foreign_source_amount' => $entry->foreign_amount, + 'description' => $entry->description, + 'source_account_id' => $entry->account_id, + 'source_account_name' => Steam::decrypt(intval($entry->account_encrypted), $entry->account_name), + 'source_account_type' => $entry->account_type, + 'source_account_before' => $sourceBalance, + 'source_account_after' => bcadd($sourceBalance, $entry->amount), + 'destination_id' => $entry->destination_id, + 'destination_amount' => bcmul($entry->amount, '-1'), + 'foreign_destination_amount' => is_null($entry->foreign_amount) ? null : bcmul($entry->foreign_amount, '-1'), + 'destination_account_id' => $entry->destination_account_id, + 'destination_account_type' => $entry->destination_account_type, + 'destination_account_name' => Steam::decrypt(intval($entry->destination_account_encrypted), $entry->destination_account_name), + 'destination_account_before' => $destinationBalance, + 'destination_account_after' => bcadd($destinationBalance, bcmul($entry->amount, '-1')), + 'budget_id' => is_null($budget) ? 0 : $budget->id, + 'category' => is_null($category) ? '' : $category->name, + 'transaction_currency_id' => $entry->transaction_currency_id, + 'transaction_currency_code' => $entry->transaction_currency_code, + 'transaction_currency_symbol' => $entry->transaction_currency_symbol, + 'foreign_currency_id' => $entry->foreign_currency_id, + 'foreign_currency_code' => $entry->foreign_currency_code, + 'foreign_currency_symbol' => $entry->foreign_currency_symbol, ]; if ($entry->destination_account_type === AccountType::CASH) { $transaction['destination_account_name'] = ''; diff --git a/app/Support/Amount.php b/app/Support/Amount.php index 32e42cf3bb..7d11d68b7d 100644 --- a/app/Support/Amount.php +++ b/app/Support/Amount.php @@ -17,6 +17,7 @@ use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\TransactionType; use Illuminate\Support\Collection; use Preferences as Prefs; @@ -101,17 +102,6 @@ class Amount return $format; } - /** - * @param string $amount - * @param bool $coloured - * - * @return string - */ - public function format(string $amount, bool $coloured = true): string - { - return $this->formatAnything($this->getDefaultCurrency(), $amount, $coloured); - } - /** * This method will properly format the given number, in color or "black and white", * as a currency, given two things: the currency required and the current locale. @@ -159,49 +149,6 @@ class Amount return $result; } - /** - * Used in many places (unfortunately). - * - * @param string $currencyCode - * @param string $amount - * @param bool $coloured - * - * @return string - */ - public function formatByCode(string $currencyCode, string $amount, bool $coloured = true): string - { - $currency = TransactionCurrency::where('code', $currencyCode)->first(); - - return $this->formatAnything($currency, $amount, $coloured); - } - - /** - * - * @param \FireflyIII\Models\TransactionJournal $journal - * @param bool $coloured - * - * @return string - */ - public function formatJournal(TransactionJournal $journal, bool $coloured = true): string - { - $currency = $journal->transactionCurrency; - - return $this->formatAnything($currency, $journal->amount(), $coloured); - } - - /** - * @param Transaction $transaction - * @param bool $coloured - * - * @return string - */ - public function formatTransaction(Transaction $transaction, bool $coloured = true) - { - $currency = $transaction->transactionJournal->transactionCurrency; - - return $this->formatAnything($currency, strval($transaction->amount), $coloured); - } - /** * @return Collection */ diff --git a/app/Support/Binder/JournalList.php b/app/Support/Binder/JournalList.php index cc31681323..3b5678f886 100644 --- a/app/Support/Binder/JournalList.php +++ b/app/Support/Binder/JournalList.php @@ -37,15 +37,8 @@ class JournalList implements BinderInterface $ids = explode(',', $value); /** @var \Illuminate\Support\Collection $object */ $object = TransactionJournal::whereIn('transaction_journals.id', $ids) - ->expanded() ->where('transaction_journals.user_id', auth()->user()->id) - ->get( - [ - 'transaction_journals.*', - 'transaction_types.type AS transaction_type_type', - 'transaction_currencies.code AS transaction_currency_code', - ] - ); + ->get(['transaction_journals.*',]); if ($object->count() > 0) { return $object; diff --git a/app/Support/Models/TransactionJournalTrait.php b/app/Support/Models/TransactionJournalTrait.php index c9cea3ec49..0961fafbb4 100644 --- a/app/Support/Models/TransactionJournalTrait.php +++ b/app/Support/Models/TransactionJournalTrait.php @@ -213,6 +213,14 @@ trait TransactionJournalTrait return 0; } + /** + * @return Transaction + */ + public function positiveTransaction(): Transaction + { + return $this->transactions()->where('amount', '>', 0)->first(); + } + /** * @return Collection */ diff --git a/app/Support/Twig/Account.php b/app/Support/Twig/Account.php deleted file mode 100644 index d2a5520505..0000000000 --- a/app/Support/Twig/Account.php +++ /dev/null @@ -1,63 +0,0 @@ -formatAmountByAccount(), - ]; - - } - - /** - * Will return "active" when a part of the route matches the argument. - * ie. "accounts" will match "accounts.index". - * - * @return Twig_SimpleFunction - */ - protected function formatAmountByAccount(): Twig_SimpleFunction - { - return new Twig_SimpleFunction( - 'formatAmountByAccount', function (AccountModel $account, string $amount, bool $coloured = true): string { - $currencyId = intval($account->getMeta('currency_id')); - if ($currencyId === 0) { - // Format using default currency: - return AmountFacade::format($amount, $coloured); - } - $currency = TransactionCurrency::find($currencyId); - - return AmountFacade::formatAnything($currency, $amount, $coloured); - }, ['is_safe' => ['html']] - ); - } - - -} \ No newline at end of file diff --git a/app/Support/Twig/AmountFormat.php b/app/Support/Twig/AmountFormat.php new file mode 100644 index 0000000000..eb06a98ebf --- /dev/null +++ b/app/Support/Twig/AmountFormat.php @@ -0,0 +1,109 @@ +formatAmount(), + $this->formatAmountPlain(), + ]; + + } + + /** + * {@inheritDoc} + */ + public function getFunctions(): array + { + return [ + $this->formatAmountByAccount(), + ]; + + } + + /** + * Returns the name of the extension. + * + * @return string The extension name + */ + public function getName(): string + { + return 'FireflyIII\Support\Twig\AmountFormat'; + } + + /** + * + * @return Twig_SimpleFilter + */ + protected function formatAmount(): Twig_SimpleFilter + { + return new Twig_SimpleFilter( + 'formatAmount', function (string $string): string { + + return app('amount')->format($string); + }, ['is_safe' => ['html']] + ); + } + + /** + * Will format the amount by the currency related to the given account. + * + * @return Twig_SimpleFunction + */ + protected function formatAmountByAccount(): Twig_SimpleFunction + { + return new Twig_SimpleFunction( + 'formatAmountByAccount', function (AccountModel $account, string $amount, bool $coloured = true): string { + $currencyId = intval($account->getMeta('currency_id')); + if ($currencyId === 0) { + // Format using default currency: + return app('amount')->format($amount, $coloured); + } + $currency = TransactionCurrency::find($currencyId); + + return app('amount')->formatAnything($currency, $amount, $coloured); + }, ['is_safe' => ['html']] + ); + } + + /** + * @return Twig_SimpleFilter + */ + protected function formatAmountPlain(): Twig_SimpleFilter + { + return new Twig_SimpleFilter( + 'formatAmountPlain', function (string $string): string { + + return app('amount')->format($string, false); + }, ['is_safe' => ['html']] + ); + } +} \ No newline at end of file diff --git a/app/Support/Twig/General.php b/app/Support/Twig/General.php index 5db0ba95f6..9603e54e44 100644 --- a/app/Support/Twig/General.php +++ b/app/Support/Twig/General.php @@ -38,9 +38,6 @@ class General extends Twig_Extension public function getFilters(): array { return [ - $this->formatAmount(), - $this->formatAmountPlain(), - $this->formatJournal(), $this->balance(), $this->formatFilesize(), $this->mimeIcon(), @@ -173,33 +170,6 @@ class General extends Twig_Extension ); } - /** - * - * @return Twig_SimpleFilter - */ - protected function formatAmount(): Twig_SimpleFilter - { - return new Twig_SimpleFilter( - 'formatAmount', function (string $string): string { - - return app('amount')->format($string); - }, ['is_safe' => ['html']] - ); - } - - /** - * @return Twig_SimpleFilter - */ - protected function formatAmountPlain(): Twig_SimpleFilter - { - return new Twig_SimpleFilter( - 'formatAmountPlain', function (string $string): string { - - return app('amount')->format($string, false); - }, ['is_safe' => ['html']] - ); - } - /** * @return Twig_SimpleFilter */ @@ -223,17 +193,6 @@ class General extends Twig_Extension ); } - /** - * @return Twig_SimpleFilter - */ - protected function formatJournal(): Twig_SimpleFilter - { - return new Twig_SimpleFilter( - 'formatJournal', function (TransactionJournal $journal): string { - return app('amount')->formatJournal($journal); - }, ['is_safe' => ['html']] - ); - } /** * @return Twig_SimpleFunction diff --git a/app/Support/Twig/Transaction.php b/app/Support/Twig/Transaction.php index d4b8e84850..a8d97c97f8 100644 --- a/app/Support/Twig/Transaction.php +++ b/app/Support/Twig/Transaction.php @@ -16,7 +16,6 @@ namespace FireflyIII\Support\Twig; use Amount; use FireflyIII\Models\AccountType; use FireflyIII\Models\Transaction as TransactionModel; -use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionType; use Steam; use Twig_Extension; @@ -30,49 +29,6 @@ use Twig_SimpleFunction; */ class Transaction extends Twig_Extension { - - /** - * @return Twig_SimpleFunction - */ - public function formatAnything(): Twig_SimpleFunction - { - return new Twig_SimpleFunction( - 'formatAnything', function (TransactionCurrency $currency, string $amount): string { - - return Amount::formatAnything($currency, $amount, true); - - }, ['is_safe' => ['html']] - ); - } - - /** - * @return Twig_SimpleFunction - */ - public function formatAnythingPlain(): Twig_SimpleFunction - { - return new Twig_SimpleFunction( - 'formatAnythingPlain', function (TransactionCurrency $currency, string $amount): string { - - return Amount::formatAnything($currency, $amount, false); - - }, ['is_safe' => ['html']] - ); - } - - /** - * @return Twig_SimpleFunction - */ - public function formatByCode(): Twig_SimpleFunction - { - return new Twig_SimpleFunction( - 'formatByCode', function (string $currencyCode, string $amount): string { - - return Amount::formatByCode($currencyCode, $amount, true); - - }, ['is_safe' => ['html']] - ); - } - /** * @return array */ @@ -91,17 +47,13 @@ class Transaction extends Twig_Extension public function getFunctions(): array { $functions = [ - $this->formatAnything(), - $this->formatAnythingPlain(), $this->transactionSourceAccount(), $this->transactionDestinationAccount(), - $this->optionalJournalAmount(), $this->transactionBudgets(), $this->transactionIdBudgets(), $this->transactionCategories(), $this->transactionIdCategories(), $this->splitJournalIndicator(), - $this->formatByCode(), ]; return $functions; @@ -117,33 +69,6 @@ class Transaction extends Twig_Extension return 'transaction'; } - /** - * @return Twig_SimpleFunction - */ - public function optionalJournalAmount(): Twig_SimpleFunction - { - return new Twig_SimpleFunction( - 'optionalJournalAmount', function (int $journalId, string $transactionAmount, string $code, string $type): string { - // get amount of journal: - $amount = strval(TransactionModel::where('transaction_journal_id', $journalId)->whereNull('deleted_at')->where('amount', '<', 0)->sum('amount')); - // display deposit and transfer positive - if ($type === TransactionType::DEPOSIT || $type === TransactionType::TRANSFER) { - $amount = bcmul($amount, '-1'); - } - - // not equal to transaction amount? - if (bccomp($amount, $transactionAmount) !== 0 && bccomp($amount, bcmul($transactionAmount, '-1')) !== 0) { - //$currency = - return sprintf(' (%s)', Amount::formatByCode($code, $amount, true)); - } - - return ''; - - - }, ['is_safe' => ['html']] - ); - } - /** * @return Twig_SimpleFunction */ diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index 283a342b20..bd8d30b2a9 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -89,6 +89,7 @@ $factory->define( 'user_id' => 1, 'transaction_type_id' => 1, 'bill_id' => null, + // TODO update this transaction currency reference. 'transaction_currency_id' => 1, 'description' => $faker->words(3, true), 'date' => '2017-01-01', diff --git a/public/js/ff/transactions/single/edit.js b/public/js/ff/transactions/single/edit.js index 17f2d0a5c0..d608568c65 100644 --- a/public/js/ff/transactions/single/edit.js +++ b/public/js/ff/transactions/single/edit.js @@ -38,11 +38,12 @@ function updateInitialPage() { $('#native_amount_holder').hide(); $('#amount_holder').hide(); - if (journalData.native_currency.id === journalData.currency.id) { + + if (journalData.native_currency.id === journalData.destination_currency.id) { $('#exchange_rate_instruction_holder').hide(); $('#destination_amount_holder').hide(); } - if (journalData.native_currency.id !== journalData.currency.id) { + if (journalData.native_currency.id !== journalData.destination_currency.id) { $('#exchange_rate_instruction_holder').show().find('p').text(getTransferExchangeInstructions()); } diff --git a/public/js/ff/transactions/split/edit.js b/public/js/ff/transactions/split/edit.js index a91c0a3422..a97739b122 100644 --- a/public/js/ff/transactions/split/edit.js +++ b/public/js/ff/transactions/split/edit.js @@ -166,11 +166,31 @@ function resetSplits() { var input = $(v); input.attr('name', 'transactions[' + i + '][amount]'); }); + + // ends with ][foreign_amount] + $.each($('input[name$="][foreign_amount]"]'), function (i, v) { + var input = $(v); + input.attr('name', 'transactions[' + i + '][foreign_amount]'); + }); + + // ends with ][transaction_currency_id] + $.each($('input[name$="][transaction_currency_id]"]'), function (i, v) { + var input = $(v); + input.attr('name', 'transactions[' + i + '][transaction_currency_id]'); + }); + + // ends with ][foreign_currency_id] + $.each($('input[name$="][foreign_currency_id]"]'), function (i, v) { + var input = $(v); + input.attr('name', 'transactions[' + i + '][foreign_currency_id]'); + }); + // ends with ][budget_id] $.each($('select[name$="][budget_id]"]'), function (i, v) { var input = $(v); input.attr('name', 'transactions[' + i + '][budget_id]'); }); + // ends with ][category] $.each($('input[name$="][category]"]'), function (i, v) { var input = $(v); diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index da1c9bcb43..54f7cbc4ae 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -962,6 +962,7 @@ return [ 'split_this_transfer' => 'Split this transfer', 'cannot_edit_multiple_source' => 'You cannot edit splitted transaction #:id with description ":description" because it contains multiple source accounts.', 'cannot_edit_multiple_dest' => 'You cannot edit splitted transaction #:id with description ":description" because it contains multiple destination accounts.', + 'cannot_edit_opening_balance' => 'You cannot edit the opening balance of an account.', 'no_edit_multiple_left' => 'You have selected no valid transactions to edit.', // import diff --git a/resources/views/list/journals-tiny.twig b/resources/views/list/journals-tiny.twig index 190d28cf37..63306fa102 100644 --- a/resources/views/list/journals-tiny.twig +++ b/resources/views/list/journals-tiny.twig @@ -14,10 +14,8 @@ {{ transaction.description }} {% endif %} - - {{ formatByCode(transaction.transaction_currency_code, transaction.transaction_amount) }} - - {{ optionalJournalAmount(transaction.journal_id, transaction.transaction_amount, transaction.transaction_currency_code, transaction.transaction_type_type) }} + {# TODO replace with new format code #} + XX.XX {% endfor %} diff --git a/resources/views/list/journals.twig b/resources/views/list/journals.twig index db348d7acb..acd95000bb 100644 --- a/resources/views/list/journals.twig +++ b/resources/views/list/journals.twig @@ -64,14 +64,11 @@ {% if transaction.transaction_type_type == 'Transfer' %} - {{ formatByCode(transaction.transaction_currency_code, steam_positive(transaction.transaction_amount)) }} - - {{ optionalJournalAmount(transaction.journal_id, transaction.transaction_amount, transaction.transaction_currency_code, transaction.transaction_type_type) }} + {# TODO format amount of transaction. #} + {# TODO format: Amount of transaction (amount in foreign) / total (total foreign) #} + XX.XX {% else %} - - {{ formatByCode(transaction.transaction_currency_code, transaction.transaction_amount) }} - - {{ optionalJournalAmount(transaction.journal_id, transaction.transaction_amount, transaction.transaction_currency_code, transaction.transaction_type_type) }} + XX.XX {% endif %} diff --git a/resources/views/popup/list/journals.twig b/resources/views/popup/list/journals.twig index d6f97dcc59..1177cce1f5 100644 --- a/resources/views/popup/list/journals.twig +++ b/resources/views/popup/list/journals.twig @@ -45,10 +45,8 @@ - - {{ formatByCode(transaction.transaction_currency_code, transaction.transaction_amount) }} - - {{ optionalJournalAmount(transaction.journal_id, transaction.transaction_amount, transaction.transaction_currency_code, transaction.transaction_type_type) }} + {# TODO replace with new format code #} + XX.XX {{ transaction.date.formatLocalized(monthAndDayFormat) }} diff --git a/resources/views/reports/partials/journals-audit.twig b/resources/views/reports/partials/journals-audit.twig index 0c9fd25adc..48c5430658 100644 --- a/resources/views/reports/partials/journals-audit.twig +++ b/resources/views/reports/partials/journals-audit.twig @@ -59,10 +59,8 @@ {{ transaction.before|formatAmount }} - - {{ formatByCode(transaction.transaction_currency_code, transaction.transaction_amount) }} - - {{ optionalJournalAmount(transaction.journal_id, transaction.transaction_amount, transaction.transaction_currency_code, transaction.transaction_type_type) }} + {# TODO replace with new format code #} + XX.XX {{ transaction.after|formatAmount }} diff --git a/resources/views/search/partials/transactions-large.twig b/resources/views/search/partials/transactions-large.twig index 7502719e27..ca8c06ac92 100644 --- a/resources/views/search/partials/transactions-large.twig +++ b/resources/views/search/partials/transactions-large.twig @@ -62,17 +62,8 @@ - {% if transaction.transaction_type_type == 'Transfer' %} - - {{ formatByCode(transaction.transaction_currency_code, steam_positive(transaction.transaction_amount)) }} - - {{ optionalJournalAmount(transaction.journal_id, transaction.transaction_amount, transaction.transaction_currency_code, transaction.transaction_type_type) }} - {% else %} - - {{ formatByCode(transaction.transaction_currency_code, transaction.transaction_amount) }} - - {{ optionalJournalAmount(transaction.journal_id, transaction.transaction_amount, transaction.transaction_currency_code, transaction.transaction_type_type) }} - {% endif %} + {# TODO replace with new format code #} + XX.XX diff --git a/resources/views/search/partials/transactions.twig b/resources/views/search/partials/transactions.twig index 587c9b432d..387b5ec07b 100644 --- a/resources/views/search/partials/transactions.twig +++ b/resources/views/search/partials/transactions.twig @@ -39,10 +39,8 @@ - - {{ formatByCode(transaction.transaction_currency_code, transaction.transaction_amount) }} - - {{ optionalJournalAmount(transaction.journal_id, transaction.transaction_amount, transaction.transaction_currency_code, transaction.transaction_type_type) }} + {# TODO replace with new format code #} + XX.XX diff --git a/resources/views/transactions/mass-delete.twig b/resources/views/transactions/mass-delete.twig index f3525c1d6c..2049df92f8 100644 --- a/resources/views/transactions/mass-delete.twig +++ b/resources/views/transactions/mass-delete.twig @@ -43,7 +43,8 @@ {{ journal.description }} - {{ journal|formatJournal }} + {# TODO fix amount display #} + XX.XX {{ journal.date.formatLocalized(monthAndDayFormat) }} diff --git a/resources/views/transactions/mass/edit.twig b/resources/views/transactions/mass/edit.twig index c0c2ccc123..a8bab41a7d 100644 --- a/resources/views/transactions/mass/edit.twig +++ b/resources/views/transactions/mass/edit.twig @@ -47,11 +47,20 @@

- {{ journal.transactionCurrency.symbol }} + {{ journal.currency_symbol }} +
- + {% if journal.foreign_amount %} + {# insert foreign data #} +
+ {{ journal.foreign_currency.symbol }} + + +
+ {% endif %} {# DATE #} diff --git a/resources/views/transactions/show.twig b/resources/views/transactions/show.twig index 3c086d1899..b115ca16c8 100644 --- a/resources/views/transactions/show.twig +++ b/resources/views/transactions/show.twig @@ -36,14 +36,9 @@ {{ 'total_amount'|_ }} - {{ journal|formatJournal }} - {% if journal.hasMeta('foreign_amount') %} - {% if journal.transactiontype.type == 'Withdrawal' %} - ({{ formatAnything(foreignCurrency, journal.getMeta('foreign_amount')*-1) }}) - {% else %} - ({{ formatAnything(foreignCurrency, journal.getMeta('foreign_amount')) }}) - {% endif %} - {% endif %} + + {# TODO fix amount display #} + XX.XX @@ -304,9 +299,8 @@ - - {{ formatAnything(journal.transactionCurrency, transaction.source_account_before) }} - ⟶ {{ formatAnything(journal.transactionCurrency, transaction.source_account_after) }} + {# TODO replace with new display: #} + XX.XX {% if transaction.destination_account_type == 'Cash account' %} @@ -317,25 +311,27 @@ - - {{ formatAnything(journal.transactionCurrency, transaction.destination_account_before) }} - ⟶ {{ formatAnything(journal.transactionCurrency, transaction.destination_account_after) }} + {# TODO replace with new format code #} + XX.XX {% if journal.transactiontype.type == 'Deposit' %} - {{ formatAnything(journal.transactionCurrency, transaction.destination_amount) }} + {# TODO replace with new format code #} + XX.XX {% endif %} {% if journal.transactiontype.type == 'Withdrawal' %} - {{ formatAnything(journal.transactionCurrency, transaction.source_amount) }} + {# TODO replace with new format code #} + XX.XX {% endif %} {% if journal.transactiontype.type == 'Transfer' %} - {{ formatAnythingPlain(journal.transactionCurrency, transaction.destination_amount) }} + {# TODO replace with new format code #} + XX.XX {% endif %} diff --git a/resources/views/transactions/single/edit.twig b/resources/views/transactions/single/edit.twig index 4ef0d2ceaa..fc09562117 100644 --- a/resources/views/transactions/single/edit.twig +++ b/resources/views/transactions/single/edit.twig @@ -64,9 +64,9 @@ {{ ExpandedForm.nonSelectableAmount('native_amount', data.native_amount, {currency: data.native_currency}) }} - {{ ExpandedForm.nonSelectableAmount('source_amount', data.native_amount, {currency: data.native_currency }) }} + {{ ExpandedForm.nonSelectableAmount('source_amount', data.source_amount, {currency: data.source_currency }) }} - {{ ExpandedForm.nonSelectableAmount('destination_amount', data.amount, {currency: data.currency }) }} + {{ ExpandedForm.nonSelectableAmount('destination_amount', data.destination_amount, {currency: data.destination_currency }) }} {# ALWAYS SHOW DATE #} {{ ExpandedForm.date('date',data['date']) }} diff --git a/resources/views/transactions/split/edit.twig b/resources/views/transactions/split/edit.twig index e78dffb4ab..a0420fe4c8 100644 --- a/resources/views/transactions/split/edit.twig +++ b/resources/views/transactions/split/edit.twig @@ -40,9 +40,6 @@ {# DESCRIPTION IS ALWAYS AVAILABLE #} {{ ExpandedForm.text('journal_description', journal.description) }} - {# CURRENCY IS NEW FOR SPLIT JOURNALS #} - {{ ExpandedForm.select('currency_id', currencies, preFilled.currency_id) }} - {# show source if withdrawal or transfer #} {% if preFilled.what == 'withdrawal' or preFilled.what == 'transfer' %} {{ ExpandedForm.select('journal_source_account_id', assetAccounts, preFilled.journal_source_account_id) }} @@ -59,6 +56,7 @@ {% endif %} {# TOTAL AMOUNT IS STATIC TEXT #} + {# TODO this does not reflect the actual currency (currencies) #} {{ ExpandedForm.staticText('journal_amount', preFilled.journal_amount|formatAmount ) }} @@ -204,7 +202,7 @@ {{ trans('list.source') }} {% endif %} - {{ trans('list.amount') }} + {{ trans('list.amount') }} {# only withdrawal has budget #} {% if preFilled.what == 'withdrawal' %} @@ -234,7 +232,7 @@ {% endif %} - + {# deposit has several source names #} {% if preFilled.what == 'deposit' %} {% endif %} - + {# two fields for amount #} - +
+
{{ transaction.transaction_currency_symbol }}
+ +
+ + + {# foreign amount #} + + {% if transaction.foreign_amount != null %} +
+
{{ transaction.foreign_currency_symbol }}
+ +
+ + {% endif %} {% if preFilled.what == 'withdrawal' %} From a487c7b4b2f8a5f35be537c821b750e7ae1d56a0 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sun, 4 Jun 2017 23:39:26 +0200 Subject: [PATCH 017/126] Make sure amounts are formatted, and fixed some issues. --- app/Helpers/Collector/JournalCollector.php | 13 +- app/Helpers/Report/PopupReport.php | 2 +- app/Http/Controllers/JsonController.php | 27 ++-- app/Http/Controllers/PiggyBankController.php | 24 ++- .../Account/AccountRepository.php | 26 +++- app/Repositories/Journal/JournalTasker.php | 4 + .../PiggyBank/PiggyBankRepository.php | 4 +- app/Support/Amount.php | 80 +++++++++- app/Support/Twig/AmountFormat.php | 141 +++++++++++++++++- resources/views/list/journals-tiny.twig | 3 +- resources/views/list/journals.twig | 9 +- resources/views/popup/list/journals.twig | 2 +- .../reports/partials/journals-audit.twig | 3 +- resources/views/transactions/mass-delete.twig | 5 +- resources/views/transactions/show.twig | 29 +--- 15 files changed, 301 insertions(+), 71 deletions(-) diff --git a/app/Helpers/Collector/JournalCollector.php b/app/Helpers/Collector/JournalCollector.php index a85535d907..fb19073685 100644 --- a/app/Helpers/Collector/JournalCollector.php +++ b/app/Helpers/Collector/JournalCollector.php @@ -65,12 +65,22 @@ class JournalCollector implements JournalCollectorInterface 'bills.name as bill_name', 'bills.name_encrypted as bill_name_encrypted', 'transactions.id as id', - 'transactions.amount as transaction_amount', + 'transactions.description as transaction_description', 'transactions.account_id', 'transactions.identifier', 'transactions.transaction_journal_id', + + 'transactions.amount as transaction_amount', 'transaction_currencies.code as transaction_currency_code', + 'transaction_currencies.symbol as transaction_currency_symbol', + 'transaction_currencies.decimal_places as transaction_currency_dp', + + 'transactions.foreign_amount as transaction_foreign_amount', + 'foreign_currencies.code as foreign_currency_code', + 'foreign_currencies.symbol as foreign_currency_symbol', + 'foreign_currencies.decimal_places as foreign_currency_dp', + 'accounts.name as account_name', 'accounts.encrypted as account_encrypted', 'account_types.type as account_type', @@ -489,6 +499,7 @@ class JournalCollector implements JournalCollectorInterface ->leftJoin('accounts', 'accounts.id', '=', 'transactions.account_id') ->leftJoin('account_types', 'accounts.account_type_id', 'account_types.id') ->leftJoin('transaction_currencies', 'transaction_currencies.id', 'transactions.transaction_currency_id') + ->leftJoin('transaction_currencies as foreign_currencies', 'foreign_currencies.id', 'transactions.foreign_currency_id') ->whereNull('transactions.deleted_at') ->whereNull('transaction_journals.deleted_at') ->where('transaction_journals.user_id', $this->user->id) diff --git a/app/Helpers/Report/PopupReport.php b/app/Helpers/Report/PopupReport.php index f4d0a07d17..265c930e6c 100644 --- a/app/Helpers/Report/PopupReport.php +++ b/app/Helpers/Report/PopupReport.php @@ -187,7 +187,7 @@ class PopupReport implements PopupReportInterface $journals = $journals->filter( function (Transaction $transaction) use ($report) { // get the destinations: - $destinations = $transaction->destinationAccountList($transaction->transactionJournal)->pluck('id')->toArray(); + $destinations = $transaction->transactionJournal->destinationAccountList()->pluck('id')->toArray(); // do these intersect with the current list? return !empty(array_intersect($report, $destinations)); diff --git a/app/Http/Controllers/JsonController.php b/app/Http/Controllers/JsonController.php index c8e916e231..4449e17b01 100644 --- a/app/Http/Controllers/JsonController.php +++ b/app/Http/Controllers/JsonController.php @@ -116,10 +116,11 @@ class JsonController extends Controller * Since both this method and the chart use the exact same data, we can suffice * with calling the one method in the bill repository that will get this amount. */ - $amount = $repository->getBillsPaidInRange($start, $end); // will be a negative amount. - $amount = bcmul($amount, '-1'); + $amount = $repository->getBillsPaidInRange($start, $end); // will be a negative amount. + $amount = bcmul($amount, '-1'); + $currency = Amount::getDefaultCurrency(); - $data = ['box' => 'bills-paid', 'amount' => Amount::format($amount, false), 'amount_raw' => $amount]; + $data = ['box' => 'bills-paid', 'amount' => Amount::formatAnything($currency, $amount, false), 'amount_raw' => $amount]; return Response::json($data); } @@ -131,10 +132,11 @@ class JsonController extends Controller */ public function boxBillsUnpaid(BillRepositoryInterface $repository) { - $start = session('start', Carbon::now()->startOfMonth()); - $end = session('end', Carbon::now()->endOfMonth()); - $amount = $repository->getBillsUnpaidInRange($start, $end); // will be a positive amount. - $data = ['box' => 'bills-unpaid', 'amount' => Amount::format($amount, false), 'amount_raw' => $amount]; + $start = session('start', Carbon::now()->startOfMonth()); + $end = session('end', Carbon::now()->endOfMonth()); + $amount = $repository->getBillsUnpaidInRange($start, $end); // will be a positive amount. + $currency = Amount::getDefaultCurrency(); + $data = ['box' => 'bills-unpaid', 'amount' => Amount::formatAnything($currency, $amount, false), 'amount_raw' => $amount]; return Response::json($data); } @@ -167,8 +169,9 @@ class JsonController extends Controller ->setTypes([TransactionType::DEPOSIT]) ->withOpposingAccount(); - $amount = strval($collector->getJournals()->sum('transaction_amount')); - $data = ['box' => 'in', 'amount' => Amount::format($amount, false), 'amount_raw' => $amount]; + $amount = strval($collector->getJournals()->sum('transaction_amount')); + $currency = Amount::getDefaultCurrency(); + $data = ['box' => 'in', 'amount' => Amount::formatAnything($currency, $amount, false), 'amount_raw' => $amount]; $cache->store($data); return Response::json($data); @@ -200,9 +203,9 @@ class JsonController extends Controller $collector->setAllAssetAccounts()->setRange($start, $end) ->setTypes([TransactionType::WITHDRAWAL]) ->withOpposingAccount(); - $amount = strval($collector->getJournals()->sum('transaction_amount')); - - $data = ['box' => 'out', 'amount' => Amount::format($amount, false), 'amount_raw' => $amount]; + $amount = strval($collector->getJournals()->sum('transaction_amount')); + $currency = Amount::getDefaultCurrency(); + $data = ['box' => 'out', 'amount' => Amount::formatAnything($currency, $amount, false), 'amount_raw' => $amount]; $cache->store($data); return Response::json($data); diff --git a/app/Http/Controllers/PiggyBankController.php b/app/Http/Controllers/PiggyBankController.php index 052900cba5..564ee720ae 100644 --- a/app/Http/Controllers/PiggyBankController.php +++ b/app/Http/Controllers/PiggyBankController.php @@ -276,18 +276,29 @@ class PiggyBankController extends Controller */ public function postAdd(Request $request, PiggyBankRepositoryInterface $repository, PiggyBank $piggyBank) { - $amount = $request->get('amount'); - + $amount = $request->get('amount'); + $currency = Amount::getDefaultCurrency(); if ($repository->canAddAmount($piggyBank, $amount)) { $repository->addAmount($piggyBank, $amount); - Session::flash('success', strval(trans('firefly.added_amount_to_piggy', ['amount' => Amount::format($amount, false), 'name' => $piggyBank->name]))); + Session::flash( + 'success', strval( + trans( + 'firefly.added_amount_to_piggy', + ['amount' => Amount::formatAnything($currency, $amount, false), 'name' => $piggyBank->name] + ) + ) + ); Preferences::mark(); return redirect(route('piggy-banks.index')); } Log::error('Cannot add ' . $amount . ' because canAddAmount returned false.'); - Session::flash('error', strval(trans('firefly.cannot_add_amount_piggy', ['amount' => Amount::format($amount, false), 'name' => e($piggyBank->name)]))); + Session::flash( + 'error', strval( + trans('firefly.cannot_add_amount_piggy', ['amount' => Amount::formatAnything($currency, $amount, false), 'name' => e($piggyBank->name)]) + ) + ); return redirect(route('piggy-banks.index')); } @@ -302,10 +313,11 @@ class PiggyBankController extends Controller public function postRemove(Request $request, PiggyBankRepositoryInterface $repository, PiggyBank $piggyBank) { $amount = $request->get('amount'); + $currency = Amount::getDefaultCurrency(); if ($repository->canRemoveAmount($piggyBank, $amount)) { $repository->removeAmount($piggyBank, $amount); Session::flash( - 'success', strval(trans('firefly.removed_amount_from_piggy', ['amount' => Amount::format($amount, false), 'name' => $piggyBank->name])) + 'success', strval(trans('firefly.removed_amount_from_piggy', ['amount' => Amount::formatAnything($currency, $amount, false), 'name' => $piggyBank->name])) ); Preferences::mark(); @@ -314,7 +326,7 @@ class PiggyBankController extends Controller $amount = strval(round($request->get('amount'), 12)); - Session::flash('error', strval(trans('firefly.cannot_remove_from_piggy', ['amount' => Amount::format($amount, false), 'name' => e($piggyBank->name)]))); + Session::flash('error', strval(trans('firefly.cannot_remove_from_piggy', ['amount' => Amount::formatAnything($currency, $amount, false), 'name' => e($piggyBank->name)]))); return redirect(route('piggy-banks.index')); } diff --git a/app/Repositories/Account/AccountRepository.php b/app/Repositories/Account/AccountRepository.php index 7b594555b8..bb9ace5034 100644 --- a/app/Repositories/Account/AccountRepository.php +++ b/app/Repositories/Account/AccountRepository.php @@ -481,7 +481,6 @@ class AccountRepository implements AccountRepositoryInterface [ 'user_id' => $this->user->id, 'transaction_type_id' => $transactionType->id, - // TODO update this transaction currency reference. 'transaction_currency_id' => $currencyId, 'description' => 'Initial balance for "' . $account->name . '"', 'completed' => true, @@ -502,9 +501,23 @@ class AccountRepository implements AccountRepositoryInterface $secondAmount = $amount; } - $one = new Transaction(['account_id' => $firstAccount->id, 'transaction_journal_id' => $journal->id, 'amount' => $firstAmount]); + $one = new Transaction( + [ + 'account_id' => $firstAccount->id, + 'transaction_journal_id' => $journal->id, + 'amount' => $firstAmount, + 'transaction_currency_id' => $currencyId, + ] + ); $one->save();// first transaction: from - $two = new Transaction(['account_id' => $secondAccount->id, 'transaction_journal_id' => $journal->id, 'amount' => $secondAmount]); + + $two = new Transaction( + [ + 'account_id' => $secondAccount->id, + 'transaction_journal_id' => $journal->id, + 'amount' => $secondAmount, + 'transaction_currency_id' => $currencyId,] + ); $two->save(); // second transaction: to Log::debug(sprintf('Stored two transactions, #%d and #%d', $one->id, $two->id)); @@ -623,18 +636,19 @@ class AccountRepository implements AccountRepositoryInterface // update date: $journal->date = $date; - // TODO update this transaction currency reference. $journal->transaction_currency_id = $currencyId; $journal->save(); // update transactions: /** @var Transaction $transaction */ foreach ($journal->transactions()->get() as $transaction) { if ($account->id == $transaction->account_id) { - $transaction->amount = $amount; + $transaction->amount = $amount; + $transaction->transaction_currency_id = $currencyId; $transaction->save(); } if ($account->id != $transaction->account_id) { - $transaction->amount = bcmul($amount, '-1'); + $transaction->amount = bcmul($amount, '-1'); + $transaction->transaction_currency_id = $currencyId; $transaction->save(); } } diff --git a/app/Repositories/Journal/JournalTasker.php b/app/Repositories/Journal/JournalTasker.php index bb0c8bcdd2..5958b0ff1c 100644 --- a/app/Repositories/Journal/JournalTasker.php +++ b/app/Repositories/Journal/JournalTasker.php @@ -101,10 +101,12 @@ class JournalTasker implements JournalTaskerInterface 'destination_accounts.encrypted as destination_account_encrypted', 'destination_account_types.type as destination_account_type', 'native_currencies.id as transaction_currency_id', + 'native_currencies.decimal_places as transaction_currency_dp', 'native_currencies.code as transaction_currency_code', 'native_currencies.symbol as transaction_currency_symbol', 'foreign_currencies.id as foreign_currency_id', + 'foreign_currencies.decimal_places as foreign_currency_dp', 'foreign_currencies.code as foreign_currency_code', 'foreign_currencies.symbol as foreign_currency_symbol', @@ -142,9 +144,11 @@ class JournalTasker implements JournalTaskerInterface 'transaction_currency_id' => $entry->transaction_currency_id, 'transaction_currency_code' => $entry->transaction_currency_code, 'transaction_currency_symbol' => $entry->transaction_currency_symbol, + 'transaction_currency_dp' => $entry->transaction_currency_dp, 'foreign_currency_id' => $entry->foreign_currency_id, 'foreign_currency_code' => $entry->foreign_currency_code, 'foreign_currency_symbol' => $entry->foreign_currency_symbol, + 'foreign_currency_dp' => $entry->foreign_currency_dp, ]; if ($entry->destination_account_type === AccountType::CASH) { $transaction['destination_account_name'] = ''; diff --git a/app/Repositories/PiggyBank/PiggyBankRepository.php b/app/Repositories/PiggyBank/PiggyBankRepository.php index e721c71b63..b5807a51f9 100644 --- a/app/Repositories/PiggyBank/PiggyBankRepository.php +++ b/app/Repositories/PiggyBank/PiggyBankRepository.php @@ -240,10 +240,12 @@ class PiggyBankRepository implements PiggyBankRepositoryInterface */ public function getPiggyBanksWithAmount(): Collection { + $currency = Amount::getDefaultCurrency(); $set = $this->getPiggyBanks(); foreach ($set as $piggy) { $currentAmount = $piggy->currentRelevantRep()->currentamount ?? '0'; - $piggy->name = $piggy->name . ' (' . Amount::format($currentAmount, false) . ')'; + + $piggy->name = $piggy->name . ' (' . Amount::formatAnything($currency, $currentAmount, false) . ')'; } return $set; diff --git a/app/Support/Amount.php b/app/Support/Amount.php index 7d11d68b7d..c75630d0ab 100644 --- a/app/Support/Amount.php +++ b/app/Support/Amount.php @@ -14,7 +14,7 @@ declare(strict_types=1); namespace FireflyIII\Support; use FireflyIII\Exceptions\FireflyException; -use FireflyIII\Models\Transaction; +use FireflyIII\Models\Transaction as TransactionModel; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; @@ -28,6 +28,7 @@ use Preferences as Prefs; */ class Amount { + /** * bool $sepBySpace is $localeconv['n_sep_by_space'] * int $signPosn = $localeconv['n_sign_posn'] @@ -242,4 +243,81 @@ class Amount 'zero' => $positive, ]; } + + /** + * @param TransactionJournal $journal + * @param bool $coloured + * + * @return string + */ + public function journalAmount(TransactionJournal $journal, bool $coloured = true): string + { + $amounts = []; + $transactions = $journal->transactions()->where('amount', '>', 0)->get(); + /** @var TransactionModel $transaction */ + foreach ($transactions as $transaction) { + // model some fields to fit "transactionAmount()": + $transaction->transaction_amount = $transaction->amount; + $transaction->transaction_foreign_amount = $transaction->foreign_amount; + $transaction->transaction_type_type = $journal->transactionType->type; + $transaction->transaction_currency_symbol = $transaction->transactionCurrency->symbol; + $transaction->transaction_currency_dp = $transaction->transactionCurrency->decimal_places; + if (!is_null($transaction->foreign_currency_id)) { + $transaction->foreign_currency_symbol = $transaction->foreignCurrency->symbol; + $transaction->foreign_currency_dp = $transaction->foreignCurrency->decimal_places; + } + + $amounts[] = $this->transactionAmount($transaction, $coloured); + } + + return join(' / ', $amounts); + + } + + /** + * This formats a transaction, IF that transaction has been "collected" using the JournalCollector. + * + * @param TransactionModel $transaction + * @param bool $coloured + * + * @return string + */ + public function transactionAmount(TransactionModel $transaction, bool $coloured = true): string + { + $amount = bcmul(app('steam')->positive(strval($transaction->transaction_amount)), '-1'); + $format = '%s'; + + if ($transaction->transaction_type_type === TransactionType::DEPOSIT) { + $amount = bcmul($amount, '-1'); + } + + if ($transaction->transaction_type_type === TransactionType::TRANSFER) { + $amount = app('steam')->positive($amount); + $coloured = false; + $format = '%s'; + } + + $currency = new TransactionCurrency; + $currency->symbol = $transaction->transaction_currency_symbol; + $currency->decimal_places = $transaction->transaction_currency_dp; + $str = sprintf($format, $this->formatAnything($currency, $amount, $coloured)); + + + if (!is_null($transaction->transaction_foreign_amount)) { + $amount = strval($transaction->transaction_foreign_amount); + + if ($transaction->transaction_type_type === TransactionType::TRANSFER) { + $amount = app('steam')->positive($amount); + $coloured = false; + $format = '%s'; + } + + $currency = new TransactionCurrency; + $currency->symbol = $transaction->foreign_currency_symbol; + $currency->decimal_places = $transaction->foreign_currency_dp; + $str .= ' (' . sprintf($format, $this->formatAnything($currency, $amount, $coloured)) . ')'; + } + + return $str; + } } diff --git a/app/Support/Twig/AmountFormat.php b/app/Support/Twig/AmountFormat.php index eb06a98ebf..cc4c45dc19 100644 --- a/app/Support/Twig/AmountFormat.php +++ b/app/Support/Twig/AmountFormat.php @@ -13,6 +13,7 @@ namespace FireflyIII\Support\Twig; use FireflyIII\Models\Account as AccountModel; +use FireflyIII\Models\Transaction as TransactionModel; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionJournal; use Twig_Extension; @@ -45,6 +46,12 @@ class AmountFormat extends Twig_Extension { return [ $this->formatAmountByAccount(), + $this->transactionAmount(), + $this->journalAmount(), + $this->formatDestinationAfter(), + $this->formatDestinationBefore(), + $this->formatSourceAfter(), + $this->formatSourceBefore(), ]; } @@ -68,7 +75,9 @@ class AmountFormat extends Twig_Extension return new Twig_SimpleFilter( 'formatAmount', function (string $string): string { - return app('amount')->format($string); + $currency = app('amount')->getDefaultCurrency(); + + return app('amount')->formatAnything($currency, $string, true); }, ['is_safe' => ['html']] ); } @@ -102,8 +111,136 @@ class AmountFormat extends Twig_Extension return new Twig_SimpleFilter( 'formatAmountPlain', function (string $string): string { - return app('amount')->format($string, false); + $currency = app('amount')->getDefaultCurrency(); + + return app('amount')->formatAnything($currency, $string, false); }, ['is_safe' => ['html']] ); } + + /** + * @return Twig_SimpleFunction + */ + protected function formatDestinationAfter(): Twig_SimpleFunction + { + return new Twig_SimpleFunction( + 'formatDestinationAfter', function (array $transaction): string { + + // build fake currency for main amount. + $format = new TransactionCurrency; + $format->decimal_places = $transaction['transaction_currency_dp']; + $format->symbol = $transaction['transaction_currency_symbol']; + $string = app('amount')->formatAnything($format, $transaction['destination_account_after'], true); + + // also append foreign amount for clarity: + if (!is_null($transaction['foreign_destination_amount'])) { + // build fake currency for foreign amount + $format = new TransactionCurrency; + $format->decimal_places = $transaction['foreign_currency_dp']; + $format->symbol = $transaction['foreign_currency_symbol']; + $string .= ' (' . app('amount')->formatAnything($format, $transaction['foreign_destination_amount'], true) . ')'; + } + + + return $string; + + }, ['is_safe' => ['html']] + ); + } + + /** + * @return Twig_SimpleFunction + */ + protected function formatDestinationBefore(): Twig_SimpleFunction + { + return new Twig_SimpleFunction( + 'formatDestinationBefore', function (array $transaction): string { + + // build fake currency for main amount. + $format = new TransactionCurrency; + $format->decimal_places = $transaction['transaction_currency_dp']; + $format->symbol = $transaction['transaction_currency_symbol']; + + return app('amount')->formatAnything($format, $transaction['destination_account_before'], true); + + }, ['is_safe' => ['html']] + ); + } + + /** + * @return Twig_SimpleFunction + */ + protected function formatSourceAfter(): Twig_SimpleFunction + { + return new Twig_SimpleFunction( + 'formatSourceAfter', function (array $transaction): string { + + // build fake currency for main amount. + $format = new TransactionCurrency; + $format->decimal_places = $transaction['transaction_currency_dp']; + $format->symbol = $transaction['transaction_currency_symbol']; + $string = app('amount')->formatAnything($format, $transaction['source_account_after'], true); + + // also append foreign amount for clarity: + if (!is_null($transaction['foreign_source_amount'])) { + // build fake currency for foreign amount + $format = new TransactionCurrency; + $format->decimal_places = $transaction['foreign_currency_dp']; + $format->symbol = $transaction['foreign_currency_symbol']; + $string .= ' (' . app('amount')->formatAnything($format, $transaction['foreign_source_amount'], true) . ')'; + } + + + return $string; + + + }, ['is_safe' => ['html']] + ); + } + + /** + * @return Twig_SimpleFunction + */ + protected function formatSourceBefore(): Twig_SimpleFunction + { + return new Twig_SimpleFunction( + 'formatSourceBefore', function (array $transaction): string { + + // build fake currency for main amount. + $format = new TransactionCurrency; + $format->decimal_places = $transaction['transaction_currency_dp']; + $format->symbol = $transaction['transaction_currency_symbol']; + + return app('amount')->formatAnything($format, $transaction['source_account_before'], true); + + }, ['is_safe' => ['html']] + ); + } + + /** + * @return Twig_SimpleFunction + */ + protected function journalAmount(): Twig_SimpleFunction + { + return new Twig_SimpleFunction( + 'journalAmount', function (TransactionJournal $journal): string { + + return app('amount')->journalAmount($journal, true); + }, ['is_safe' => ['html']] + ); + } + + /** + * @return Twig_SimpleFunction + */ + protected function transactionAmount(): Twig_SimpleFunction + { + return new Twig_SimpleFunction( + 'transactionAmount', function (TransactionModel $transaction): string { + + return app('amount')->transactionAmount($transaction, true); + }, ['is_safe' => ['html']] + ); + } + } \ No newline at end of file diff --git a/resources/views/list/journals-tiny.twig b/resources/views/list/journals-tiny.twig index 63306fa102..ed6ee40b30 100644 --- a/resources/views/list/journals-tiny.twig +++ b/resources/views/list/journals-tiny.twig @@ -14,8 +14,7 @@ {{ transaction.description }} {% endif %} - {# TODO replace with new format code #} - XX.XX + {{ transactionAmount(transaction) }} {% endfor %} diff --git a/resources/views/list/journals.twig b/resources/views/list/journals.twig index acd95000bb..02fd791df8 100644 --- a/resources/views/list/journals.twig +++ b/resources/views/list/journals.twig @@ -62,14 +62,7 @@ - {% if transaction.transaction_type_type == 'Transfer' %} - - {# TODO format amount of transaction. #} - {# TODO format: Amount of transaction (amount in foreign) / total (total foreign) #} - XX.XX - {% else %} - XX.XX - {% endif %} + {{ transactionAmount(transaction) }} diff --git a/resources/views/popup/list/journals.twig b/resources/views/popup/list/journals.twig index 1177cce1f5..1ab88a9f32 100644 --- a/resources/views/popup/list/journals.twig +++ b/resources/views/popup/list/journals.twig @@ -46,7 +46,7 @@ {# TODO replace with new format code #} - XX.XX + {{ transactionAmount(transaction) }} {{ transaction.date.formatLocalized(monthAndDayFormat) }} diff --git a/resources/views/reports/partials/journals-audit.twig b/resources/views/reports/partials/journals-audit.twig index 48c5430658..4f0f794f96 100644 --- a/resources/views/reports/partials/journals-audit.twig +++ b/resources/views/reports/partials/journals-audit.twig @@ -59,8 +59,7 @@ {{ transaction.before|formatAmount }} - {# TODO replace with new format code #} - XX.XX + {{ transactionAmount(transaction) }} {{ transaction.after|formatAmount }} diff --git a/resources/views/transactions/mass-delete.twig b/resources/views/transactions/mass-delete.twig index 2049df92f8..864ea084ae 100644 --- a/resources/views/transactions/mass-delete.twig +++ b/resources/views/transactions/mass-delete.twig @@ -29,7 +29,7 @@   {{ trans('list.description') }} - {{ trans('list.amount') }} + {{ trans('list.total_amount') }} {{ trans('list.date') }} {{ trans('list.from') }} {{ trans('list.to') }} @@ -43,8 +43,7 @@ {{ journal.description }} - {# TODO fix amount display #} - XX.XX + {{ journalAmount(journal) }} {{ journal.date.formatLocalized(monthAndDayFormat) }} diff --git a/resources/views/transactions/show.twig b/resources/views/transactions/show.twig index b115ca16c8..a6a7fc8876 100644 --- a/resources/views/transactions/show.twig +++ b/resources/views/transactions/show.twig @@ -37,8 +37,7 @@ {{ 'total_amount'|_ }} - {# TODO fix amount display #} - XX.XX + {{ journalAmount(journal) }} @@ -299,8 +298,7 @@ - {# TODO replace with new display: #} - XX.XX + {{ formatSourceBefore(transaction) }} → {{ formatSourceAfter(transaction) }} {% if transaction.destination_account_type == 'Cash account' %} @@ -311,29 +309,10 @@ - {# TODO replace with new format code #} - XX.XX + {{ formatDestinationBefore(transaction) }} → {{ formatDestinationAfter(transaction) }} - {% if journal.transactiontype.type == 'Deposit' %} - - {# TODO replace with new format code #} - XX.XX - - {% endif %} - {% if journal.transactiontype.type == 'Withdrawal' %} - - {# TODO replace with new format code #} - XX.XX - - {% endif %} - {% if journal.transactiontype.type == 'Transfer' %} - - - {# TODO replace with new format code #} - XX.XX - - {% endif %} + {{ journalAmount(journal) }} {{ transactionIdBudgets(transaction.source_id) }} From 0b47e5d05d6f3cea944f9a5a97318e0903f632e4 Mon Sep 17 00:00:00 2001 From: James Cole Date: Mon, 5 Jun 2017 07:03:20 +0200 Subject: [PATCH 018/126] Removed unnecessary variable. --- app/Console/Commands/UpgradeDatabase.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/Console/Commands/UpgradeDatabase.php b/app/Console/Commands/UpgradeDatabase.php index 2c3a20f3a0..7a185d004b 100644 --- a/app/Console/Commands/UpgradeDatabase.php +++ b/app/Console/Commands/UpgradeDatabase.php @@ -82,9 +82,8 @@ class UpgradeDatabase extends Command */ private function currencyInfoToTransactions() { - $count = 0; - $expanded = 0; - $set = TransactionJournal::with('transactions')->get(); + $count = 0; + $set = TransactionJournal::with('transactions')->get(); /** @var TransactionJournal $journal */ foreach ($set as $journal) { /** @var Transaction $transaction */ From f72f8b03dff74c03025a2a897b78a844073bf2a8 Mon Sep 17 00:00:00 2001 From: James Cole Date: Mon, 5 Jun 2017 07:03:32 +0200 Subject: [PATCH 019/126] Catch empty currency preference --- app/Support/Twig/AmountFormat.php | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/Support/Twig/AmountFormat.php b/app/Support/Twig/AmountFormat.php index cc4c45dc19..ba7011f902 100644 --- a/app/Support/Twig/AmountFormat.php +++ b/app/Support/Twig/AmountFormat.php @@ -92,13 +92,17 @@ class AmountFormat extends Twig_Extension return new Twig_SimpleFunction( 'formatAmountByAccount', function (AccountModel $account, string $amount, bool $coloured = true): string { $currencyId = intval($account->getMeta('currency_id')); - if ($currencyId === 0) { - // Format using default currency: - return app('amount')->format($amount, $coloured); + + if ($currencyId !== 0) { + $currency = TransactionCurrency::find($currencyId); + + return app('amount')->formatAnything($currency, $amount, $coloured); } - $currency = TransactionCurrency::find($currencyId); + $currency = app('amount')->getDefaultCurrency(); return app('amount')->formatAnything($currency, $amount, $coloured); + + }, ['is_safe' => ['html']] ); } From 1dec270907ced01323fb5556e18b5c7ac17601ac Mon Sep 17 00:00:00 2001 From: James Cole Date: Mon, 5 Jun 2017 07:37:53 +0200 Subject: [PATCH 020/126] These changes fix the tests. --- app/Repositories/Journal/JournalTasker.php | 4 +- database/factories/ModelFactory.php | 44 +++++++----- .../Controllers/JsonControllerTest.php | 21 +++--- .../Transaction/SplitControllerTest.php | 72 +++++++++---------- 4 files changed, 75 insertions(+), 66 deletions(-) diff --git a/app/Repositories/Journal/JournalTasker.php b/app/Repositories/Journal/JournalTasker.php index 5958b0ff1c..5aacaa94ac 100644 --- a/app/Repositories/Journal/JournalTasker.php +++ b/app/Repositories/Journal/JournalTasker.php @@ -144,11 +144,11 @@ class JournalTasker implements JournalTaskerInterface 'transaction_currency_id' => $entry->transaction_currency_id, 'transaction_currency_code' => $entry->transaction_currency_code, 'transaction_currency_symbol' => $entry->transaction_currency_symbol, - 'transaction_currency_dp' => $entry->transaction_currency_dp, + 'transaction_currency_dp' => $entry->transaction_currency_dp, 'foreign_currency_id' => $entry->foreign_currency_id, 'foreign_currency_code' => $entry->foreign_currency_code, 'foreign_currency_symbol' => $entry->foreign_currency_symbol, - 'foreign_currency_dp' => $entry->foreign_currency_dp, + 'foreign_currency_dp' => $entry->foreign_currency_dp, ]; if ($entry->destination_account_type === AccountType::CASH) { $transaction['destination_account_name'] = ''; diff --git a/database/factories/ModelFactory.php b/database/factories/ModelFactory.php index bd8d30b2a9..2727d4d86c 100644 --- a/database/factories/ModelFactory.php +++ b/database/factories/ModelFactory.php @@ -217,25 +217,31 @@ $factory->define( $factory->define( FireflyIII\Models\Transaction::class, function (Faker\Generator $faker) { return [ - 'transaction_amount' => strval($faker->randomFloat(2, -100, 100)), - 'destination_amount' => strval($faker->randomFloat(2, -100, 100)), - 'opposing_account_id' => $faker->numberBetween(1, 10), - 'source_account_id' => $faker->numberBetween(1, 10), - 'opposing_account_name' => $faker->words(3, true), - 'description' => $faker->words(3, true), - 'source_account_name' => $faker->words(3, true), - 'destination_account_id' => $faker->numberBetween(1, 10), - 'date' => new Carbon, - 'destination_account_name' => $faker->words(3, true), - 'amount' => strval($faker->randomFloat(2, -100, 100)), - 'budget_id' => 0, - 'category' => $faker->words(3, true), - 'transaction_journal_id' => $faker->numberBetween(1, 10), - 'journal_id' => $faker->numberBetween(1, 10), - 'transaction_currency_code' => 'EUR', - 'transaction_type_type' => 'Withdrawal', - 'account_encrypted' => 0, - 'account_name' => 'Some name', + 'transaction_amount' => strval($faker->randomFloat(2, -100, 100)), + 'destination_amount' => strval($faker->randomFloat(2, -100, 100)), + 'opposing_account_id' => $faker->numberBetween(1, 10), + 'source_account_id' => $faker->numberBetween(1, 10), + 'opposing_account_name' => $faker->words(3, true), + 'description' => $faker->words(3, true), + 'source_account_name' => $faker->words(3, true), + 'destination_account_id' => $faker->numberBetween(1, 10), + 'date' => new Carbon, + 'destination_account_name' => $faker->words(3, true), + 'amount' => strval($faker->randomFloat(2, -100, 100)), + 'budget_id' => 0, + 'category' => $faker->words(3, true), + 'transaction_journal_id' => $faker->numberBetween(1, 10), + 'journal_id' => $faker->numberBetween(1, 10), + 'transaction_currency_code' => 'EUR', + 'transaction_type_type' => 'Withdrawal', + 'account_encrypted' => 0, + 'account_name' => 'Some name', + 'transaction_currency_id' => 1, + 'transaction_currency_symbol' => '€', + 'foreign_destination_amount' => null, + 'foreign_currency_id' => null, + 'foreign_currency_code' => null, + 'foreign_currency_symbol' => null, ]; } ); \ No newline at end of file diff --git a/tests/Feature/Controllers/JsonControllerTest.php b/tests/Feature/Controllers/JsonControllerTest.php index 798575bbef..22c4152d77 100644 --- a/tests/Feature/Controllers/JsonControllerTest.php +++ b/tests/Feature/Controllers/JsonControllerTest.php @@ -21,7 +21,6 @@ use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; -use FireflyIII\Repositories\Account\AccountTaskerInterface; use FireflyIII\Repositories\Bill\BillRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Category\CategoryRepositoryInterface; @@ -88,15 +87,17 @@ class JsonControllerTest extends TestCase public function testBoxBillsPaid() { // mock stuff + $billRepos = $this->mock(BillRepositoryInterface::class); $journalRepos = $this->mock(JournalRepositoryInterface::class); $journalRepos->shouldReceive('first')->once()->andReturn(new TransactionJournal); $billRepos->shouldReceive('getBillsPaidInRange')->andReturn('-100'); $this->be($this->user()); + $currency = Amount::getDefaultCurrency(); $response = $this->get(route('json.box.paid')); $response->assertStatus(200); - $response->assertExactJson(['amount' => Amount::format('100', false), 'amount_raw' => '100', 'box' => 'bills-paid']); + $response->assertExactJson(['amount' => Amount::formatAnything($currency, '100', false), 'amount_raw' => '100', 'box' => 'bills-paid']); } /** @@ -105,6 +106,7 @@ class JsonControllerTest extends TestCase public function testBoxBillsUnpaid() { // mock stuff + $billRepos = $this->mock(BillRepositoryInterface::class); $journalRepos = $this->mock(JournalRepositoryInterface::class); $journalRepos->shouldReceive('first')->once()->andReturn(new TransactionJournal); @@ -112,9 +114,10 @@ class JsonControllerTest extends TestCase $this->be($this->user()); + $currency = Amount::getDefaultCurrency(); $response = $this->get(route('json.box.unpaid')); $response->assertStatus(200); - $response->assertExactJson(['amount' => Amount::format('100', false), 'amount_raw' => '100', 'box' => 'bills-unpaid']); + $response->assertExactJson(['amount' => Amount::formatAnything($currency, '100', false), 'amount_raw' => '100', 'box' => 'bills-unpaid']); } /** @@ -123,8 +126,7 @@ class JsonControllerTest extends TestCase public function testBoxIn() { // mock stuff - $accountRepos = $this->mock(AccountRepositoryInterface::class); - $tasker = $this->mock(AccountTaskerInterface::class); + $collector = $this->mock(JournalCollectorInterface::class); $journalRepos = $this->mock(JournalRepositoryInterface::class); $journalRepos->shouldReceive('first')->once()->andReturn(new TransactionJournal); @@ -139,9 +141,10 @@ class JsonControllerTest extends TestCase $this->be($this->user()); + $currency = Amount::getDefaultCurrency(); $response = $this->get(route('json.box.in')); $response->assertStatus(200); - $response->assertExactJson(['amount' => Amount::format('100', false), 'amount_raw' => '100', 'box' => 'in']); + $response->assertExactJson(['amount' => Amount::formatAnything($currency, '100', false), 'amount_raw' => '100', 'box' => 'in']); } /** @@ -150,8 +153,7 @@ class JsonControllerTest extends TestCase public function testBoxOut() { // mock stuff - $accountRepos = $this->mock(AccountRepositoryInterface::class); - $tasker = $this->mock(AccountTaskerInterface::class); + $collector = $this->mock(JournalCollectorInterface::class); $journalRepos = $this->mock(JournalRepositoryInterface::class); $journalRepos->shouldReceive('first')->once()->andReturn(new TransactionJournal); @@ -165,9 +167,10 @@ class JsonControllerTest extends TestCase $collector->shouldReceive('withOpposingAccount')->andReturnSelf()->once(); $this->be($this->user()); + $currency = Amount::getDefaultCurrency(); $response = $this->get(route('json.box.out')); $response->assertStatus(200); - $response->assertExactJson(['amount' => Amount::format('100', false), 'amount_raw' => '100', 'box' => 'out']); + $response->assertExactJson(['amount' => Amount::formatAnything($currency, '100', false), 'amount_raw' => '100', 'box' => 'out']); } /** diff --git a/tests/Feature/Controllers/Transaction/SplitControllerTest.php b/tests/Feature/Controllers/Transaction/SplitControllerTest.php index 00c5a5b3c6..578802bc8c 100644 --- a/tests/Feature/Controllers/Transaction/SplitControllerTest.php +++ b/tests/Feature/Controllers/Transaction/SplitControllerTest.php @@ -7,7 +7,7 @@ * See the LICENSE file for details. */ -declare(strict_types = 1); +declare(strict_types=1); namespace Tests\Feature\Controllers\Transaction; @@ -32,37 +32,6 @@ use Tests\TestCase; */ class SplitControllerTest extends TestCase { - /** - * @covers \FireflyIII\Http\Controllers\Transaction\SplitController::edit - * @covers \FireflyIII\Http\Controllers\Transaction\SplitController::__construct - * @covers \FireflyIII\Http\Controllers\Transaction\SplitController::arrayFromJournal - * @covers \FireflyIII\Http\Controllers\Transaction\SplitController::getTransactionDataFromJournal - */ - public function testEditSingle() - { - - $currencyRepository = $this->mock(CurrencyRepositoryInterface::class); - $accountRepository = $this->mock(AccountRepositoryInterface::class); - $budgetRepository = $this->mock(BudgetRepositoryInterface::class); - $transactions = factory(Transaction::class, 1)->make(); - $tasker = $this->mock(JournalTaskerInterface::class); - - $currencyRepository->shouldReceive('get')->once()->andReturn(new Collection); - $accountRepository->shouldReceive('getAccountsByType')->withArgs([[AccountType::DEFAULT, AccountType::ASSET]]) - ->andReturn(new Collection)->once(); - $budgetRepository->shouldReceive('getActiveBudgets')->andReturn(new Collection); - $tasker->shouldReceive('getTransactionsOverview')->andReturn($transactions->toArray()); - - - $deposit = TransactionJournal::where('transaction_type_id', 2)->where('user_id', $this->user()->id)->first(); - $this->be($this->user()); - $response = $this->get(route('transactions.split.edit', [$deposit->id])); - $response->assertStatus(200); - // has bread crumb - $response->assertSee('
diff --git a/resources/views/reports/partials/journals-audit.twig b/resources/views/reports/partials/journals-audit.twig index 4f0f794f96..d5da12c69f 100644 --- a/resources/views/reports/partials/journals-audit.twig +++ b/resources/views/reports/partials/journals-audit.twig @@ -57,11 +57,11 @@ {% endif %} - {{ transaction.before|formatAmount }} + {{ formatAmountByCurrency(transaction.currency, transaction.before) }} {{ transactionAmount(transaction) }} - {{ transaction.after|formatAmount }} + {{ formatAmountByCurrency(transaction.currency, transaction.after) }} {{ transaction.date.formatLocalized(monthAndDayFormat) }} From 0e929602a801af11e875a085a686a2e189b5ecee Mon Sep 17 00:00:00 2001 From: James Cole Date: Tue, 6 Jun 2017 07:23:54 +0200 Subject: [PATCH 029/126] Verify currency data routine. --- app/Console/Commands/UpgradeDatabase.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/app/Console/Commands/UpgradeDatabase.php b/app/Console/Commands/UpgradeDatabase.php index 7a185d004b..654267b9d0 100644 --- a/app/Console/Commands/UpgradeDatabase.php +++ b/app/Console/Commands/UpgradeDatabase.php @@ -74,6 +74,7 @@ class UpgradeDatabase extends Command $this->updateAccountCurrencies(); $this->updateJournalCurrencies(); $this->currencyInfoToTransactions(); + $this->verifyCurrencyInfo(); $this->info('Firefly III database is up to date.'); } @@ -386,4 +387,25 @@ class UpgradeDatabase extends Command } } } + + /** + * + */ + private function verifyCurrencyInfo() + { + $count = 0; + $transactions = Transaction::get(); + /** @var Transaction $transaction */ + foreach ($transactions as $transaction) { + $currencyId = intval($transaction->transaction_currency_id); + $foreignId = intval($transaction->foreign_currency_id); + if ($currencyId === $foreignId) { + $transaction->foreign_currency_id = null; + $transaction->foreign_amount = null; + $transaction->save(); + $count++; + } + } + $this->line(sprintf('Updated currency information for %d transactions', $count)); + } } From 65ccb2d443e116d9e72f6216f92a3ca3742eaea4 Mon Sep 17 00:00:00 2001 From: James Cole Date: Tue, 6 Jun 2017 19:29:10 +0200 Subject: [PATCH 030/126] Fix error display #662 --- public/js/ff/export/index.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/public/js/ff/export/index.js b/public/js/ff/export/index.js index c7d0f3fd40..c04d89f2cc 100644 --- a/public/js/ff/export/index.js +++ b/public/js/ff/export/index.js @@ -104,10 +104,22 @@ function callExport() { // show download showDownload(); - }).fail(function () { + }).fail(function (data) { // show error. // show form again. - showError('The export failed. Please check the log files to find out why.'); + + var errorText = 'The export failed. Please check the log files to find out why.'; + if (typeof data.responseJSON === 'object') { + errorText = ''; + for (var propt in data.responseJSON) { + if (data.responseJSON.hasOwnProperty(propt)) { + errorText += propt + ': ' + data.responseJSON[propt][0]; + } + } + } + + showError(errorText); + // stop polling: window.clearTimeout(intervalId); From a8ec4fe2fd8f4195e03d8cf5fa319e60e5e5cdcf Mon Sep 17 00:00:00 2001 From: James Cole Date: Tue, 6 Jun 2017 19:30:31 +0200 Subject: [PATCH 031/126] New interface for budget overview. --- app/Console/Commands/UpgradeDatabase.php | 2 +- app/Http/Controllers/BudgetController.php | 37 +++- resources/views/budgets/index.twig | 219 ++++++++++++++-------- routes/web.php | 2 +- 4 files changed, 172 insertions(+), 88 deletions(-) diff --git a/app/Console/Commands/UpgradeDatabase.php b/app/Console/Commands/UpgradeDatabase.php index 654267b9d0..107f61f604 100644 --- a/app/Console/Commands/UpgradeDatabase.php +++ b/app/Console/Commands/UpgradeDatabase.php @@ -389,7 +389,7 @@ class UpgradeDatabase extends Command } /** - * + * */ private function verifyCurrencyInfo() { diff --git a/app/Http/Controllers/BudgetController.php b/app/Http/Controllers/BudgetController.php index 05188a146e..bad2b980dc 100644 --- a/app/Http/Controllers/BudgetController.php +++ b/app/Http/Controllers/BudgetController.php @@ -15,6 +15,7 @@ namespace FireflyIII\Http\Controllers; use Amount; use Carbon\Carbon; +use Exception; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Http\Requests\BudgetFormRequest; @@ -166,16 +167,36 @@ class BudgetController extends Controller } /** + * @param string|null $moment + * * @return View */ - public function index() + public function index(string $moment = null) { + $range = Preferences::get('viewRange', '1M')->data; + $start = session('start', new Carbon); + $end = session('end', new Carbon); + + // make date if present: + if (!is_null($moment) || strlen(strval($moment)) !== 0) { + try { + $start = new Carbon($moment); + $end = Navigation::endOfPeriod($start, $range); + } catch (Exception $e) { + + } + } + $next = clone $end; + $next->addDay(); + $prev = clone $start; + $prev->subDay(); + + $this->repository->cleanupBudgets(); + $budgets = $this->repository->getActiveBudgets(); $inactive = $this->repository->getInactiveBudgets(); - $start = session('start', new Carbon); - $end = session('end', new Carbon); $periodStart = $start->formatLocalized($this->monthAndDayFormat); $periodEnd = $end->formatLocalized($this->monthAndDayFormat); $budgetInformation = $this->collectBudgetInformation($budgets, $start, $end); @@ -184,9 +205,17 @@ class BudgetController extends Controller $spent = array_sum(array_column($budgetInformation, 'spent')); $budgeted = array_sum(array_column($budgetInformation, 'budgeted')); + // display info + $currentMonth = Navigation::periodShow($start, $range); + $nextText = Navigation::periodShow($next, $range); + $prevText = Navigation::periodShow($prev, $range); + return view( 'budgets.index', - compact('available', 'periodStart', 'periodEnd', 'budgetInformation', 'inactive', 'budgets', 'spent', 'budgeted') + compact( + 'available', 'currentMonth', 'next', 'nextText', 'prev', 'prevText', 'periodStart', 'periodEnd', 'budgetInformation', 'inactive', 'budgets', + 'spent', 'budgeted' + ) ); } diff --git a/resources/views/budgets/index.twig b/resources/views/budgets/index.twig index 9ecec09211..8f2823cd0f 100644 --- a/resources/views/budgets/index.twig +++ b/resources/views/budgets/index.twig @@ -87,96 +87,151 @@ {% if budgets.count == 0 and inactive.count == 0 %} {% include 'partials.empty' with {what: 'default', type: 'budgets',route: route('budgets.create')} %} {% endif %} + + {# date thing #}
- {% for budget in budgets %} -
+
-

- - {% if budgetInformation[budget.id]['currentLimit'] %} - {{ budget.name }} - {% else %} - {{ budget.name }} - {% endif %} -

- - -
-
- - +

Period thing

+
+
+
+
+ +
+
+ +
+
+
-
- + + + + +
+
+
+
+

Budget stuff

+
+
+
+ + + + + + + + + + + + {% for budget in budgets %} - - - - + + + {# +
+
 Budget{{ 'budgeted'|_ }}SpentLeft
- {{ 'budgeted'|_ }} -
- {{ session('start').formatLocalized(monthAndDayFormat) }} - - {{ session('end').formatLocalized(monthAndDayFormat) }}
-
-
-
-
{{ defaultCurrency.symbol|raw }}
- - {% if budgetInformation[budget.id]['currentLimit'] %} - {% set repAmount = budgetInformation[budget.id]['currentLimit'].amount %} - {% else %} - {% set repAmount = '0' %} - {% endif %} - -
+
+ +
- {{ 'spent'|_ }} -
- {{ session('start').formatLocalized(monthAndDayFormat) }} - - {{ session('end').formatLocalized(monthAndDayFormat) }} -
+
+ {% if budgetInformation[budget.id]['currentLimit'] %} + {{ budget.name }} + {% else %} + {{ budget.name }} + {% endif %} + +
+
{{ defaultCurrency.symbol|raw }}
+ + {% if budgetInformation[budget.id]['currentLimit'] %} + {% set repAmount = budgetInformation[budget.id]['currentLimit'].amount %} + {% else %} + {% set repAmount = '0' %} + {% endif %} + +
{{ budgetInformation[budget.id]['spent']|formatAmount }} + {{ (repAmount - budgetInformation[budget.id]['spent'])|formatAmount }} +
+ + + + + + + + + {% if budgetInformation[budget.id]['otherLimits'].count > 0 %} + + + + {% endif %} +
+ + + +
+ +
+
+ {{ 'spent'|_ }} +
+ {{ session('start').formatLocalized(monthAndDayFormat) }} - + {{ session('end').formatLocalized(monthAndDayFormat) }} +
+
+ +
+
    + {% for other in budgetInformation[budget.id]['otherLimits'] %} +
  • + + Budgeted + {{ other.amount|formatAmountPlain }} + between + {{ other.start_date.formatLocalized(monthAndDayFormat) }} + and {{ other.end_date.formatLocalized(monthAndDayFormat) }}. +
  • + {% endfor %} +
+
+
+
+
+ #} - {% if budgetInformation[budget.id]['otherLimits'].count > 0 %} - - -
    - {% for other in budgetInformation[budget.id]['otherLimits'] %} -
  • - - Budgeted - {{ other.amount|formatAmountPlain }} - between - {{ other.start_date.formatLocalized(monthAndDayFormat) }} - and {{ other.end_date.formatLocalized(monthAndDayFormat) }}. -
  • - {% endfor %} -
- - - {% endif %} + {% endfor %} +
- {% if loop.index % 3 == 0 %} -
- {% endif %} - {% endfor %}
{% if inactive|length > 0 %} @@ -200,16 +255,16 @@
{% endif %} {% endblock %} -{% block scripts %} - + // budgeted data: + var budgeted = {{ budgeted }}; + var available = {{ available }}; + - -{% endblock %} + + {% endblock %} diff --git a/routes/web.php b/routes/web.php index 4b480c3b1c..d8e969e4ac 100755 --- a/routes/web.php +++ b/routes/web.php @@ -134,7 +134,7 @@ Route::group( */ Route::group( ['middleware' => 'user-full-auth', 'prefix' => 'budgets', 'as' => 'budgets.'], function () { - Route::get('', ['uses' => 'BudgetController@index', 'as' => 'index']); + Route::get('{moment?}', ['uses' => 'BudgetController@index', 'as' => 'index']); Route::get('income', ['uses' => 'BudgetController@updateIncome', 'as' => 'income']); Route::get('create', ['uses' => 'BudgetController@create', 'as' => 'create']); Route::get('edit/{budget}', ['uses' => 'BudgetController@edit', 'as' => 'edit']); From 9d5d1c0a419630b903215c65baebb808329eef38 Mon Sep 17 00:00:00 2001 From: James Cole Date: Tue, 6 Jun 2017 20:35:39 +0200 Subject: [PATCH 032/126] Updated budget view. --- app/Http/Controllers/BudgetController.php | 29 ++++++++++++++++++++++- public/js/ff/budgets/index.js | 11 ++++++++- resources/lang/en_US/firefly.php | 5 ++-- resources/views/budgets/index.twig | 23 +++++++++++------- 4 files changed, 56 insertions(+), 12 deletions(-) diff --git a/app/Http/Controllers/BudgetController.php b/app/Http/Controllers/BudgetController.php index bad2b980dc..565d9bfe8b 100644 --- a/app/Http/Controllers/BudgetController.php +++ b/app/Http/Controllers/BudgetController.php @@ -190,6 +190,7 @@ class BudgetController extends Controller $next->addDay(); $prev = clone $start; $prev->subDay(); + $prev = Navigation::startOfPeriod($prev, $range); $this->repository->cleanupBudgets(); @@ -205,6 +206,32 @@ class BudgetController extends Controller $spent = array_sum(array_column($budgetInformation, 'spent')); $budgeted = array_sum(array_column($budgetInformation, 'budgeted')); + // select thing for last 12 periods: + $previousLoop = []; + $previousDate = clone $start; + $count = 0; + while ($count < 12) { + $previousDate->subDay(); + $previousDate = Navigation::startOfPeriod($previousDate, $range); + $format = $previousDate->format('Y-m-d'); + $previousLoop[$format] = Navigation::periodShow($previousDate, $range); + $count++; + } + + // select thing for next 12 periods: + $nextLoop = []; + $nextDate = clone $end; + $nextDate->addDay(); + $count = 0; + + while ($count < 12) { + $format = $nextDate->format('Y-m-d'); + $nextLoop[$format] = Navigation::periodShow($nextDate, $range); + $nextDate = Navigation::endOfPeriod($nextDate, $range); + $count++; + $nextDate->addDay(); + } + // display info $currentMonth = Navigation::periodShow($start, $range); $nextText = Navigation::periodShow($next, $range); @@ -214,7 +241,7 @@ class BudgetController extends Controller 'budgets.index', compact( 'available', 'currentMonth', 'next', 'nextText', 'prev', 'prevText', 'periodStart', 'periodEnd', 'budgetInformation', 'inactive', 'budgets', - 'spent', 'budgeted' + 'spent', 'budgeted', 'previousLoop', 'nextLoop', 'start' ) ); } diff --git a/public/js/ff/budgets/index.js b/public/js/ff/budgets/index.js index 936ae07b7f..ac233b85c4 100644 --- a/public/js/ff/budgets/index.js +++ b/public/js/ff/budgets/index.js @@ -8,7 +8,7 @@ * See the LICENSE file for details. */ -/** global: spent, budgeted, available, currencySymbol */ +/** global: spent, budgeted, available, currencySymbol, budgetIndexURI */ function drawSpentBar() { "use strict"; @@ -99,6 +99,15 @@ $(function () { */ $('input[type="number"]').on('input', updateBudgetedAmounts); + // + $('.selectPeriod').change(function (e) { + var sel = $(e.target).val(); + if (sel !== "x") { + var newURI = budgetIndexURI.replace("REPLACE", sel); + window.location.assign(newURI); + } + }); + }); function updateIncome() { diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index 54f7cbc4ae..501c03430a 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -20,6 +20,7 @@ return [ 'everything' => 'Everything', 'customRange' => 'Custom range', 'apply' => 'Apply', + 'select_date' => 'Select date..', 'cancel' => 'Cancel', 'from' => 'From', 'to' => 'To', @@ -112,8 +113,8 @@ return [ 'budget_in_period' => 'All transactions for budget ":name" between :start and :end', 'chart_budget_in_period' => 'Chart for all transactions for budget ":name" between :start and :end', 'chart_account_in_period' => 'Chart for all transactions for account ":name" between :start and :end', - 'chart_category_in_period' => 'Chart for all transactions for category ":name" between :start and :end', - 'chart_category_all' => 'Chart for all transactions for category ":name"', + 'chart_category_in_period' => 'Chart for all transactions for category ":name" between :start and :end', + 'chart_category_all' => 'Chart for all transactions for category ":name"', 'budget_in_period_breadcrumb' => 'Between :start and :end', 'clone_withdrawal' => 'Clone this withdrawal', 'clone_deposit' => 'Clone this deposit', diff --git a/resources/views/budgets/index.twig b/resources/views/budgets/index.twig index 8f2823cd0f..7f1f5ffc4e 100644 --- a/resources/views/budgets/index.twig +++ b/resources/views/budgets/index.twig @@ -98,20 +98,26 @@
- + + {% for format, previousLabel in previousLoop %} + + {% endfor %}
- + + {% for format, nextLabel in nextLoop %} + + {% endfor %}
@@ -131,10 +137,10 @@   - Budget + {{ 'budget'|_ }} {{ 'budgeted'|_ }} - Spent - Left + {{ 'spent'|_ }} + {{ 'left'|_ }} @@ -264,6 +270,7 @@ // budgeted data: var budgeted = {{ budgeted }}; var available = {{ available }}; + var budgetIndexURI = "{{ route('budgets.index','REPLACE') }}"; From 51ddcd9ee1de78fdfe5ecc7b21e5b095095ec97e Mon Sep 17 00:00:00 2001 From: James Cole Date: Tue, 6 Jun 2017 20:37:24 +0200 Subject: [PATCH 033/126] Plus not minus [skip ci] --- resources/views/budgets/index.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/budgets/index.twig b/resources/views/budgets/index.twig index 7f1f5ffc4e..240b7e9845 100644 --- a/resources/views/budgets/index.twig +++ b/resources/views/budgets/index.twig @@ -180,7 +180,7 @@ {{ budgetInformation[budget.id]['spent']|formatAmount }} - {{ (repAmount - budgetInformation[budget.id]['spent'])|formatAmount }} + {{ (repAmount + budgetInformation[budget.id]['spent'])|formatAmount }} {#
From e5db5a7b5ce16fa14b61d9ba61f6a37a23ea4aad Mon Sep 17 00:00:00 2001 From: James Cole Date: Wed, 7 Jun 2017 07:38:58 +0200 Subject: [PATCH 034/126] Various code clean up. --- app/Http/Controllers/BudgetController.php | 1 + app/Http/Controllers/Chart/AccountController.php | 2 +- .../Transaction/ConvertController.php | 16 ++++++++++------ app/Support/Search/Search.php | 2 +- app/Support/Steam.php | 8 ++------ public/js/ff/budgets/show.js | 2 +- public/js/ff/transactions/single/edit.js | 2 -- test.sh | 4 ++++ 8 files changed, 20 insertions(+), 17 deletions(-) diff --git a/app/Http/Controllers/BudgetController.php b/app/Http/Controllers/BudgetController.php index 565d9bfe8b..1e4545d518 100644 --- a/app/Http/Controllers/BudgetController.php +++ b/app/Http/Controllers/BudgetController.php @@ -183,6 +183,7 @@ class BudgetController extends Controller $start = new Carbon($moment); $end = Navigation::endOfPeriod($start, $range); } catch (Exception $e) { + // start and end are already defined. } } diff --git a/app/Http/Controllers/Chart/AccountController.php b/app/Http/Controllers/Chart/AccountController.php index 81fcf35c81..c0af003e81 100644 --- a/app/Http/Controllers/Chart/AccountController.php +++ b/app/Http/Controllers/Chart/AccountController.php @@ -349,7 +349,7 @@ class AccountController extends Controller $cache->addProperty('chart.account.period'); $cache->addProperty($account->id); if ($cache->has()) { - //return Response::json($cache->get()); // @codeCoverageIgnore + return Response::json($cache->get()); // @codeCoverageIgnore } $format = (string)trans('config.month_and_day'); diff --git a/app/Http/Controllers/Transaction/ConvertController.php b/app/Http/Controllers/Transaction/ConvertController.php index 8853a3dbaa..2b1e8bf061 100644 --- a/app/Http/Controllers/Transaction/ConvertController.php +++ b/app/Http/Controllers/Transaction/ConvertController.php @@ -174,14 +174,17 @@ class ConvertController extends Controller switch ($joined) { default: throw new FireflyException('Cannot handle ' . $joined); // @codeCoverageIgnore - case TransactionType::WITHDRAWAL . '-' . TransactionType::DEPOSIT: // one + case TransactionType::WITHDRAWAL . '-' . TransactionType::DEPOSIT: + // one $destination = $sourceAccount; break; - case TransactionType::WITHDRAWAL . '-' . TransactionType::TRANSFER: // two + case TransactionType::WITHDRAWAL . '-' . TransactionType::TRANSFER: + // two $destination = $accountRepository->find(intval($data['destination_account_asset'])); break; - case TransactionType::DEPOSIT . '-' . TransactionType::WITHDRAWAL: // three - case TransactionType::TRANSFER . '-' . TransactionType::WITHDRAWAL: // five + case TransactionType::DEPOSIT . '-' . TransactionType::WITHDRAWAL: + case TransactionType::TRANSFER . '-' . TransactionType::WITHDRAWAL: + // three and five if ($data['destination_account_expense'] === '') { // destination is a cash account. $destination = $accountRepository->getCashAccount(); @@ -197,8 +200,9 @@ class ConvertController extends Controller ]; $destination = $accountRepository->store($data); break; - case TransactionType::DEPOSIT . '-' . TransactionType::TRANSFER: // four - case TransactionType::TRANSFER . '-' . TransactionType::DEPOSIT: // six + case TransactionType::DEPOSIT . '-' . TransactionType::TRANSFER: + case TransactionType::TRANSFER . '-' . TransactionType::DEPOSIT: + // four and six $destination = $destinationAccount; break; } diff --git a/app/Support/Search/Search.php b/app/Support/Search/Search.php index 4b9ffacb31..ebceaf59f9 100644 --- a/app/Support/Search/Search.php +++ b/app/Support/Search/Search.php @@ -53,7 +53,7 @@ class Search implements SearchInterface public function __construct() { $this->modifiers = new Collection; - $this->validModifiers = config('firefly.search_modifiers'); + $this->validModifiers = (array) config('firefly.search_modifiers'); } /** diff --git a/app/Support/Steam.php b/app/Support/Steam.php index e97b0f2c03..398400e05b 100644 --- a/app/Support/Steam.php +++ b/app/Support/Steam.php @@ -135,7 +135,7 @@ class Steam $cache->addProperty($start); $cache->addProperty($end); if ($cache->has()) { - //return $cache->get(); // @codeCoverageIgnore + return $cache->get(); // @codeCoverageIgnore } $start->subDay(); @@ -167,10 +167,6 @@ class Steam ] ); -// echo '
';
-//        var_dump($set->toArray());
-//        exit;
-
         $currentBalance = $startBalance;
         /** @var Transaction $entry */
         foreach ($set as $entry) {
@@ -217,7 +213,7 @@ class Steam
         $cache->addProperty('balances');
         $cache->addProperty($date);
         if ($cache->has()) {
-            //return $cache->get(); // @codeCoverageIgnore
+            return $cache->get(); // @codeCoverageIgnore
         }
 
         // need to do this per account.
diff --git a/public/js/ff/budgets/show.js b/public/js/ff/budgets/show.js
index 1852c5b6cd..01dda2660e 100644
--- a/public/js/ff/budgets/show.js
+++ b/public/js/ff/budgets/show.js
@@ -8,7 +8,7 @@
  * See the LICENSE file for details.
  */
 
-/** global: budgetChartUri, expenseCategoryUri, expenseAssetUri, expenseExpenseUri */
+/** global: budgetChartUri, expenseCategoryUri, expenseAssetUri, expenseExpenseUri, budgetLimitID */
 
 $(function () {
     "use strict";
diff --git a/public/js/ff/transactions/single/edit.js b/public/js/ff/transactions/single/edit.js
index 05c2132260..7d9308e1cb 100644
--- a/public/js/ff/transactions/single/edit.js
+++ b/public/js/ff/transactions/single/edit.js
@@ -32,8 +32,6 @@ $(document).ready(function () {
  */
 function updateInitialPage() {
 
-    console.log('Native currency is #' + journalData.native_currency.id + ' and (foreign) currency id is #' + journalData.currency.id);
-
     if (journal.transaction_type.type === "Transfer") {
         $('#native_amount_holder').hide();
         $('#amount_holder').hide();
diff --git a/test.sh b/test.sh
index 177dc25cb9..90e472cd80 100755
--- a/test.sh
+++ b/test.sh
@@ -87,6 +87,10 @@ then
 
     # call test data generation script
     $(which php) /sites/FF3/test-data/artisan generate:data local sqlite
+
+    # also run upgrade routine:
+    $(which php) /sites/FF3/firefly-iii/artisan firefly:upgrade-database
+
     # copy new database over backup (resets backup)
     cp $DATABASE $DATABASECOPY
 fi

From 92c5cabd7090162c8b9d66c2b02c26f14154bafd Mon Sep 17 00:00:00 2001
From: James Cole 
Date: Wed, 7 Jun 2017 08:18:42 +0200
Subject: [PATCH 035/126] Try to untangle complex repositories

---
 .../Transaction/MassController.php            |   5 +-
 .../Transaction/SingleController.php          |  11 +-
 .../Transaction/SplitController.php           |  12 +-
 app/Providers/JournalServiceProvider.php      |  22 --
 .../Account/AccountRepository.php             | 189 +-----------
 .../Account/FindAccountsTrait.php             | 212 +++++++++++++
 .../Journal/CreateJournalsTrait.php           | 186 +++++++++++
 .../Journal/JournalRepository.php             | 126 ++++++--
 .../Journal/JournalRepositoryInterface.php    |  16 +
 app/Repositories/Journal/JournalUpdate.php    | 290 ------------------
 .../Journal/JournalUpdateInterface.php        |  44 ---
 ...alSupport.php => SupportJournalsTrait.php} | 137 +--------
 .../Journal/UpdateJournalsTrait.php           | 156 ++++++++++
 13 files changed, 699 insertions(+), 707 deletions(-)
 create mode 100644 app/Repositories/Account/FindAccountsTrait.php
 create mode 100644 app/Repositories/Journal/CreateJournalsTrait.php
 delete mode 100644 app/Repositories/Journal/JournalUpdate.php
 delete mode 100644 app/Repositories/Journal/JournalUpdateInterface.php
 rename app/Repositories/Journal/{JournalSupport.php => SupportJournalsTrait.php} (65%)
 create mode 100644 app/Repositories/Journal/UpdateJournalsTrait.php

diff --git a/app/Http/Controllers/Transaction/MassController.php b/app/Http/Controllers/Transaction/MassController.php
index 61d2a0fa43..8b3abddfce 100644
--- a/app/Http/Controllers/Transaction/MassController.php
+++ b/app/Http/Controllers/Transaction/MassController.php
@@ -23,7 +23,6 @@ use FireflyIII\Models\TransactionType;
 use FireflyIII\Repositories\Account\AccountRepositoryInterface;
 use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
 use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
-use FireflyIII\Repositories\Journal\JournalUpdateInterface;
 use Illuminate\Support\Collection;
 use Preferences;
 use Session;
@@ -204,7 +203,7 @@ class MassController extends Controller
      *
      * @return mixed
      */
-    public function update(MassEditJournalRequest $request, JournalRepositoryInterface $repository, JournalUpdateInterface $updater)
+    public function update(MassEditJournalRequest $request, JournalRepositoryInterface $repository)
     {
         $journalIds = $request->get('journals');
         $count      = 0;
@@ -250,7 +249,7 @@ class MassController extends Controller
                         'tags'                     => $tags,
                     ];
                     // call repository update function.
-                    $updater->update($journal, $data);
+                    $repository->update($journal, $data);
 
                     $count++;
                 }
diff --git a/app/Http/Controllers/Transaction/SingleController.php b/app/Http/Controllers/Transaction/SingleController.php
index 67e2c7cb19..75bb6a3c8b 100644
--- a/app/Http/Controllers/Transaction/SingleController.php
+++ b/app/Http/Controllers/Transaction/SingleController.php
@@ -27,7 +27,6 @@ use FireflyIII\Repositories\Account\AccountRepositoryInterface;
 use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
 use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
 use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
-use FireflyIII\Repositories\Journal\JournalUpdateInterface;
 use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface;
 use Log;
 use Preferences;
@@ -365,13 +364,13 @@ class SingleController extends Controller
     }
 
     /**
-     * @param JournalFormRequest     $request
-     * @param JournalUpdateInterface $updater
-     * @param TransactionJournal     $journal
+     * @param JournalFormRequest         $request
+     * @param JournalRepositoryInterface $repository
+     * @param TransactionJournal         $journal
      *
      * @return $this|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
      */
-    public function update(JournalFormRequest $request, JournalUpdateInterface $updater, TransactionJournal $journal)
+    public function update(JournalFormRequest $request, JournalRepositoryInterface $repository, TransactionJournal $journal)
     {
         // @codeCoverageIgnoreStart
         if ($this->isOpeningBalance($journal)) {
@@ -380,7 +379,7 @@ class SingleController extends Controller
         // @codeCoverageIgnoreEnd
 
         $data    = $request->getJournalData();
-        $journal = $updater->update($journal, $data);
+        $journal = $repository->update($journal, $data);
         /** @var array $files */
         $files = $request->hasFile('attachments') ? $request->file('attachments') : null;
         $this->attachments->saveAttachmentsForModel($journal, $files);
diff --git a/app/Http/Controllers/Transaction/SplitController.php b/app/Http/Controllers/Transaction/SplitController.php
index fa899d6bff..e6668bc43e 100644
--- a/app/Http/Controllers/Transaction/SplitController.php
+++ b/app/Http/Controllers/Transaction/SplitController.php
@@ -23,8 +23,8 @@ use FireflyIII\Models\TransactionJournal;
 use FireflyIII\Repositories\Account\AccountRepositoryInterface;
 use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
 use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
+use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
 use FireflyIII\Repositories\Journal\JournalTaskerInterface;
-use FireflyIII\Repositories\Journal\JournalUpdateInterface;
 use Illuminate\Http\Request;
 use Log;
 use Preferences;
@@ -122,20 +122,20 @@ class SplitController extends Controller
 
 
     /**
-     * @param Request                $request
-     * @param JournalUpdateInterface $updater
-     * @param TransactionJournal     $journal
+     * @param Request                    $request
+     * @param JournalRepositoryInterface $repository
+     * @param TransactionJournal         $journal
      *
      * @return $this|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
      */
-    public function update(Request $request, JournalUpdateInterface $updater, TransactionJournal $journal)
+    public function update(Request $request, JournalRepositoryInterface $repository, TransactionJournal $journal)
     {
         if ($this->isOpeningBalance($journal)) {
             return $this->redirectToAccount($journal);
         }
 
         $data    = $this->arrayFromInput($request);
-        $journal = $updater->updateSplitJournal($journal, $data);
+        $journal = $repository->updateSplitJournal($journal, $data);
         /** @var array $files */
         $files = $request->hasFile('attachments') ? $request->file('attachments') : null;
         // save attachments:
diff --git a/app/Providers/JournalServiceProvider.php b/app/Providers/JournalServiceProvider.php
index 7fcabbe606..d221714c21 100644
--- a/app/Providers/JournalServiceProvider.php
+++ b/app/Providers/JournalServiceProvider.php
@@ -20,8 +20,6 @@ use FireflyIII\Repositories\Journal\JournalRepository;
 use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
 use FireflyIII\Repositories\Journal\JournalTasker;
 use FireflyIII\Repositories\Journal\JournalTaskerInterface;
-use FireflyIII\Repositories\Journal\JournalUpdate;
-use FireflyIII\Repositories\Journal\JournalUpdateInterface;
 use Illuminate\Foundation\Application;
 use Illuminate\Support\ServiceProvider;
 
@@ -52,7 +50,6 @@ class JournalServiceProvider extends ServiceProvider
         $this->registerRepository();
         $this->registerTasker();
         $this->registerCollector();
-        $this->registerUpdater();
     }
 
     /**
@@ -115,23 +112,4 @@ class JournalServiceProvider extends ServiceProvider
         );
     }
 
-    /**
-     *
-     */
-    private function registerUpdater()
-    {
-        $this->app->bind(
-            JournalUpdateInterface::class,
-            function (Application $app) {
-                /** @var JournalUpdateInterface $tasker */
-                $update = app(JournalUpdate::class);
-
-                if ($app->auth->check()) {
-                    $update->setUser(auth()->user());
-                }
-
-                return $update;
-            }
-        );
-    }
 }
diff --git a/app/Repositories/Account/AccountRepository.php b/app/Repositories/Account/AccountRepository.php
index bb9ace5034..c7d6422a50 100644
--- a/app/Repositories/Account/AccountRepository.php
+++ b/app/Repositories/Account/AccountRepository.php
@@ -24,8 +24,6 @@ use FireflyIII\Models\Transaction;
 use FireflyIII\Models\TransactionJournal;
 use FireflyIII\Models\TransactionType;
 use FireflyIII\User;
-use Illuminate\Database\Eloquent\Relations\HasMany;
-use Illuminate\Support\Collection;
 use Log;
 
 
@@ -37,6 +35,7 @@ use Log;
  */
 class AccountRepository implements AccountRepositoryInterface
 {
+    use FindAccountsTrait;
 
     /** @var User */
     private $user;
@@ -77,192 +76,6 @@ class AccountRepository implements AccountRepositoryInterface
         return true;
     }
 
-    /**
-     * @param $accountId
-     *
-     * @return Account
-     */
-    public function find(int $accountId): Account
-    {
-        $account = $this->user->accounts()->find($accountId);
-        if (is_null($account)) {
-            return new Account;
-        }
-
-        return $account;
-    }
-
-    /**
-     * @param string $number
-     * @param array  $types
-     *
-     * @return Account
-     */
-    public function findByAccountNumber(string $number, array $types): Account
-    {
-        $query = $this->user->accounts()
-                            ->leftJoin('account_meta', 'account_meta.account_id', '=', 'accounts.id')
-                            ->where('account_meta.name', 'accountNumber')
-                            ->where('account_meta.data', json_encode($number));
-
-        if (count($types) > 0) {
-            $query->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id');
-            $query->whereIn('account_types.type', $types);
-        }
-
-        /** @var Collection $accounts */
-        $accounts = $query->get(['accounts.*']);
-        if ($accounts->count() > 0) {
-            return $accounts->first();
-        }
-
-        return new Account;
-    }
-
-    /**
-     * @param string $iban
-     * @param array  $types
-     *
-     * @return Account
-     */
-    public function findByIban(string $iban, array $types): Account
-    {
-        $query = $this->user->accounts()->where('iban', '!=', '')->whereNotNull('iban');
-
-        if (count($types) > 0) {
-            $query->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id');
-            $query->whereIn('account_types.type', $types);
-        }
-
-        $accounts = $query->get(['accounts.*']);
-        /** @var Account $account */
-        foreach ($accounts as $account) {
-            if ($account->iban === $iban) {
-                return $account;
-            }
-        }
-
-        return new Account;
-    }
-
-    /**
-     * @param string $name
-     * @param array  $types
-     *
-     * @return Account
-     */
-    public function findByName(string $name, array $types): Account
-    {
-        $query = $this->user->accounts();
-
-        if (count($types) > 0) {
-            $query->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id');
-            $query->whereIn('account_types.type', $types);
-
-        }
-        Log::debug(sprintf('Searching for account named %s of the following type(s)', $name), ['types' => $types]);
-
-        $accounts = $query->get(['accounts.*']);
-        /** @var Account $account */
-        foreach ($accounts as $account) {
-            if ($account->name === $name) {
-                Log::debug(sprintf('Found #%d (%s) with type id %d', $account->id, $account->name, $account->account_type_id));
-
-                return $account;
-            }
-        }
-        Log::debug('Found nothing.');
-
-        return new Account;
-    }
-
-    /**
-     * @param array $accountIds
-     *
-     * @return Collection
-     */
-    public function getAccountsById(array $accountIds): Collection
-    {
-        /** @var Collection $result */
-        $query = $this->user->accounts();
-
-        if (count($accountIds) > 0) {
-            $query->whereIn('accounts.id', $accountIds);
-        }
-
-        $result = $query->get(['accounts.*']);
-        $result = $result->sortBy(
-            function (Account $account) {
-                return strtolower($account->name);
-            }
-        );
-
-        return $result;
-    }
-
-    /**
-     * @param array $types
-     *
-     * @return Collection
-     */
-    public function getAccountsByType(array $types): Collection
-    {
-        /** @var Collection $result */
-        $query = $this->user->accounts();
-        if (count($types) > 0) {
-            $query->accountTypeIn($types);
-        }
-
-        $result = $query->get(['accounts.*']);
-        $result = $result->sortBy(
-            function (Account $account) {
-                return strtolower($account->name);
-            }
-        );
-
-        return $result;
-    }
-
-    /**
-     * @param array $types
-     *
-     * @return Collection
-     */
-    public function getActiveAccountsByType(array $types): Collection
-    {
-        /** @var Collection $result */
-        $query = $this->user->accounts()->with(
-            ['accountmeta' => function (HasMany $query) {
-                $query->where('name', 'accountRole');
-            }]
-        );
-        if (count($types) > 0) {
-            $query->accountTypeIn($types);
-        }
-        $query->where('active', 1);
-        $result = $query->get(['accounts.*']);
-        $result = $result->sortBy(
-            function (Account $account) {
-                return strtolower($account->name);
-            }
-        );
-
-        return $result;
-    }
-
-    /**
-     * @return Account
-     */
-    public function getCashAccount(): Account
-    {
-        $type    = AccountType::where('type', AccountType::CASH)->first();
-        $account = Account::firstOrCreateEncrypted(
-            ['user_id' => $this->user->id, 'account_type_id' => $type->id, 'name' => 'Cash account', 'active' => 1]
-        );
-
-        return $account;
-    }
-
     /**
      * Returns the date of the very last transaction in this account.
      *
diff --git a/app/Repositories/Account/FindAccountsTrait.php b/app/Repositories/Account/FindAccountsTrait.php
new file mode 100644
index 0000000000..062c7c1b8f
--- /dev/null
+++ b/app/Repositories/Account/FindAccountsTrait.php
@@ -0,0 +1,212 @@
+user->accounts()->find($accountId);
+        if (is_null($account)) {
+            return new Account;
+        }
+
+        return $account;
+    }
+
+    /**
+     * @param string $number
+     * @param array  $types
+     *
+     * @return Account
+     */
+    public function findByAccountNumber(string $number, array $types): Account
+    {
+        $query = $this->user->accounts()
+                            ->leftJoin('account_meta', 'account_meta.account_id', '=', 'accounts.id')
+                            ->where('account_meta.name', 'accountNumber')
+                            ->where('account_meta.data', json_encode($number));
+
+        if (count($types) > 0) {
+            $query->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id');
+            $query->whereIn('account_types.type', $types);
+        }
+
+        /** @var Collection $accounts */
+        $accounts = $query->get(['accounts.*']);
+        if ($accounts->count() > 0) {
+            return $accounts->first();
+        }
+
+        return new Account;
+    }
+
+    /**
+     * @param string $iban
+     * @param array  $types
+     *
+     * @return Account
+     */
+    public function findByIban(string $iban, array $types): Account
+    {
+        $query = $this->user->accounts()->where('iban', '!=', '')->whereNotNull('iban');
+
+        if (count($types) > 0) {
+            $query->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id');
+            $query->whereIn('account_types.type', $types);
+        }
+
+        $accounts = $query->get(['accounts.*']);
+        /** @var Account $account */
+        foreach ($accounts as $account) {
+            if ($account->iban === $iban) {
+                return $account;
+            }
+        }
+
+        return new Account;
+    }
+
+    /**
+     * @param string $name
+     * @param array  $types
+     *
+     * @return Account
+     */
+    public function findByName(string $name, array $types): Account
+    {
+        $query = $this->user->accounts();
+
+        if (count($types) > 0) {
+            $query->leftJoin('account_types', 'accounts.account_type_id', '=', 'account_types.id');
+            $query->whereIn('account_types.type', $types);
+
+        }
+        Log::debug(sprintf('Searching for account named %s of the following type(s)', $name), ['types' => $types]);
+
+        $accounts = $query->get(['accounts.*']);
+        /** @var Account $account */
+        foreach ($accounts as $account) {
+            if ($account->name === $name) {
+                Log::debug(sprintf('Found #%d (%s) with type id %d', $account->id, $account->name, $account->account_type_id));
+
+                return $account;
+            }
+        }
+        Log::debug('Found nothing.');
+
+        return new Account;
+    }
+
+    /**
+     * @param array $accountIds
+     *
+     * @return Collection
+     */
+    public function getAccountsById(array $accountIds): Collection
+    {
+        /** @var Collection $result */
+        $query = $this->user->accounts();
+
+        if (count($accountIds) > 0) {
+            $query->whereIn('accounts.id', $accountIds);
+        }
+
+        $result = $query->get(['accounts.*']);
+        $result = $result->sortBy(
+            function (Account $account) {
+                return strtolower($account->name);
+            }
+        );
+
+        return $result;
+    }
+
+    /**
+     * @param array $types
+     *
+     * @return Collection
+     */
+    public function getAccountsByType(array $types): Collection
+    {
+        /** @var Collection $result */
+        $query = $this->user->accounts();
+        if (count($types) > 0) {
+            $query->accountTypeIn($types);
+        }
+
+        $result = $query->get(['accounts.*']);
+        $result = $result->sortBy(
+            function (Account $account) {
+                return strtolower($account->name);
+            }
+        );
+
+        return $result;
+    }
+
+    /**
+     * @param array $types
+     *
+     * @return Collection
+     */
+    public function getActiveAccountsByType(array $types): Collection
+    {
+        /** @var Collection $result */
+        $query = $this->user->accounts()->with(
+            ['accountmeta' => function (HasMany $query) {
+                $query->where('name', 'accountRole');
+            }]
+        );
+        if (count($types) > 0) {
+            $query->accountTypeIn($types);
+        }
+        $query->where('active', 1);
+        $result = $query->get(['accounts.*']);
+        $result = $result->sortBy(
+            function (Account $account) {
+                return strtolower($account->name);
+            }
+        );
+
+        return $result;
+    }
+
+    /**
+     * @return Account
+     */
+    public function getCashAccount(): Account
+    {
+        $type    = AccountType::where('type', AccountType::CASH)->first();
+        $account = Account::firstOrCreateEncrypted(
+            ['user_id' => $this->user->id, 'account_type_id' => $type->id, 'name' => 'Cash account', 'active' => 1]
+        );
+
+        return $account;
+    }
+}
\ No newline at end of file
diff --git a/app/Repositories/Journal/CreateJournalsTrait.php b/app/Repositories/Journal/CreateJournalsTrait.php
new file mode 100644
index 0000000000..dbfa818e84
--- /dev/null
+++ b/app/Repositories/Journal/CreateJournalsTrait.php
@@ -0,0 +1,186 @@
+ 0) {
+                $tag = Tag::firstOrCreateEncrypted(['tag' => $name, 'user_id' => $journal->user_id]);
+                if (!is_null($tag)) {
+                    Log::debug(sprintf('Will try to connect tag #%d to journal #%d.', $tag->id, $journal->id));
+                    $tagRepository->connect($journal, $tag);
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * @param Transaction $transaction
+     * @param int         $budgetId
+     */
+    protected function storeBudgetWithTransaction(Transaction $transaction, int $budgetId)
+    {
+        if (intval($budgetId) > 0 && $transaction->transactionJournal->transactionType->type !== TransactionType::TRANSFER) {
+            /** @var \FireflyIII\Models\Budget $budget */
+            $budget = Budget::find($budgetId);
+            $transaction->budgets()->save($budget);
+        }
+    }
+
+    /**
+     * @param Transaction $transaction
+     * @param string      $category
+     */
+    protected function storeCategoryWithTransaction(Transaction $transaction, string $category)
+    {
+        if (strlen($category) > 0) {
+            $category = Category::firstOrCreateEncrypted(['name' => $category, 'user_id' => $transaction->transactionJournal->user_id]);
+            $transaction->categories()->save($category);
+        }
+    }
+
+    /**
+     * The reference to storeAccounts() in this function is an indication of spagetti code but alas,
+     * I leave it as it is.
+     *
+     * @param TransactionJournal $journal
+     * @param array              $transaction
+     * @param int                $identifier
+     *
+     * @return Collection
+     */
+    protected function storeSplitTransaction(TransactionJournal $journal, array $transaction, int $identifier): Collection
+    {
+        // store source and destination accounts (depends on type)
+        $accounts = $this->storeAccounts($this->user, $journal->transactionType, $transaction);
+
+        // store transaction one way:
+        $amount        = bcmul(strval($transaction['amount']), '-1');
+        $foreignAmount = is_null($transaction['foreign_amount']) ? null : bcmul(strval($transaction['foreign_amount']), '-1');
+        $one           = $this->storeTransaction(
+            [
+                'journal'                 => $journal,
+                'account'                 => $accounts['source'],
+                'amount'                  => $amount,
+                'transaction_currency_id' => $transaction['transaction_currency_id'],
+                'foreign_amount'          => $foreignAmount,
+                'foreign_currency_id'     => $transaction['foreign_currency_id'],
+                'description'             => $transaction['description'],
+                'category'                => null,
+                'budget'                  => null,
+                'identifier'              => $identifier,
+            ]
+        );
+        $this->storeCategoryWithTransaction($one, $transaction['category']);
+        $this->storeBudgetWithTransaction($one, $transaction['budget_id']);
+
+        // and the other way:
+        $amount        = strval($transaction['amount']);
+        $foreignAmount = is_null($transaction['foreign_amount']) ? null : strval($transaction['foreign_amount']);
+        $two           = $this->storeTransaction(
+            [
+                'journal'                 => $journal,
+                'account'                 => $accounts['destination'],
+                'amount'                  => $amount,
+                'transaction_currency_id' => $transaction['transaction_currency_id'],
+                'foreign_amount'          => $foreignAmount,
+                'foreign_currency_id'     => $transaction['foreign_currency_id'],
+                'description'             => $transaction['description'],
+                'category'                => null,
+                'budget'                  => null,
+                'identifier'              => $identifier,
+            ]
+        );
+        $this->storeCategoryWithTransaction($two, $transaction['category']);
+        $this->storeBudgetWithTransaction($two, $transaction['budget_id']);
+
+        return new Collection([$one, $two]);
+    }
+
+    /**
+     * @param array $data
+     *
+     * @return Transaction
+     */
+    protected function storeTransaction(array $data): Transaction
+    {
+        $fields = [
+            'transaction_journal_id'  => $data['journal']->id,
+            'account_id'              => $data['account']->id,
+            'amount'                  => $data['amount'],
+            'foreign_amount'          => $data['foreign_amount'],
+            'transaction_currency_id' => $data['transaction_currency_id'],
+            'foreign_currency_id'     => $data['foreign_currency_id'],
+            'description'             => $data['description'],
+            'identifier'              => $data['identifier'],
+        ];
+
+
+        if (is_null($data['foreign_currency_id'])) {
+            unset($fields['foreign_currency_id']);
+        }
+        if (is_null($data['foreign_amount'])) {
+            unset($fields['foreign_amount']);
+        }
+
+        /** @var Transaction $transaction */
+        $transaction = Transaction::create($fields);
+
+        Log::debug(sprintf('Transaction stored with ID: %s', $transaction->id));
+
+        if (!is_null($data['category'])) {
+            $transaction->categories()->save($data['category']);
+        }
+
+        if (!is_null($data['budget'])) {
+            $transaction->categories()->save($data['budget']);
+        }
+
+        return $transaction;
+
+    }
+
+}
\ No newline at end of file
diff --git a/app/Repositories/Journal/JournalRepository.php b/app/Repositories/Journal/JournalRepository.php
index d8402430a1..774ae77947 100644
--- a/app/Repositories/Journal/JournalRepository.php
+++ b/app/Repositories/Journal/JournalRepository.php
@@ -14,10 +14,8 @@ declare(strict_types=1);
 namespace FireflyIII\Repositories\Journal;
 
 use FireflyIII\Models\Account;
-use FireflyIII\Models\Tag;
 use FireflyIII\Models\TransactionJournal;
 use FireflyIII\Models\TransactionType;
-use FireflyIII\Repositories\Tag\TagRepositoryInterface;
 use FireflyIII\User;
 use Illuminate\Support\Collection;
 use Illuminate\Support\MessageBag;
@@ -31,6 +29,8 @@ use Preferences;
  */
 class JournalRepository implements JournalRepositoryInterface
 {
+    use CreateJournalsTrait, UpdateJournalsTrait, SupportJournalsTrait;
+
     /** @var User */
     private $user;
     /** @var array */
@@ -170,8 +170,8 @@ class JournalRepository implements JournalRepositoryInterface
         // find transaction type.
         /** @var TransactionType $transactionType */
         $transactionType = TransactionType::where('type', ucfirst($data['what']))->first();
-        $accounts        = JournalSupport::storeAccounts($this->user, $transactionType, $data);
-        $data            = JournalSupport::verifyNativeAmount($data, $accounts);
+        $accounts        = $this->storeAccounts($this->user, $transactionType, $data);
+        $data            = $this->verifyNativeAmount($data, $accounts);
         $amount          = strval($data['amount']);
         $journal         = new TransactionJournal(
             [
@@ -186,8 +186,8 @@ class JournalRepository implements JournalRepositoryInterface
         $journal->save();
 
         // store stuff:
-        JournalSupport::storeCategoryWithJournal($journal, $data['category']);
-        JournalSupport::storeBudgetWithJournal($journal, $data['budget_id']);
+        $this->storeCategoryWithJournal($journal, $data['category']);
+        $this->storeBudgetWithJournal($journal, $data['budget_id']);
 
         // store two transactions:
         $one = [
@@ -202,7 +202,7 @@ class JournalRepository implements JournalRepositoryInterface
             'budget'                  => null,
             'identifier'              => 0,
         ];
-        JournalSupport::storeTransaction($one);
+        $this->storeTransaction($one);
 
         $two = [
             'journal'                 => $journal,
@@ -217,7 +217,7 @@ class JournalRepository implements JournalRepositoryInterface
             'identifier'              => 0,
         ];
 
-        JournalSupport::storeTransaction($two);
+        $this->storeTransaction($two);
 
 
         // store tags
@@ -241,30 +241,112 @@ class JournalRepository implements JournalRepositoryInterface
     }
 
     /**
+     * @param TransactionJournal $journal
+     * @param array              $data
      *
-     * * Remember: a balancingAct takes at most one expense and one transfer.
-     *            an advancePayment takes at most one expense, infinite deposits and NO transfers.
+     * @return TransactionJournal
+     */
+    public function update(TransactionJournal $journal, array $data): TransactionJournal
+    {
+
+        // update actual journal:
+        $journal->description   = $data['description'];
+        $journal->date          = $data['date'];
+        $accounts               = $this->storeAccounts($this->user, $journal->transactionType, $data);
+        $data                   = $this->verifyNativeAmount($data, $accounts);
+        $data['amount']         = strval($data['amount']);
+        $data['foreign_amount'] = is_null($data['foreign_amount']) ? null : strval($data['foreign_amount']);
+
+        // unlink all categories, recreate them:
+        $journal->categories()->detach();
+        $journal->budgets()->detach();
+
+        $this->storeCategoryWithJournal($journal, $data['category']);
+        $this->storeBudgetWithJournal($journal, $data['budget_id']);
+
+        // negative because source loses money.
+        $this->updateSourceTransaction($journal, $accounts['source'], $data);
+
+        // positive because destination gets money.
+        $this->updateDestinationTransaction($journal, $accounts['destination'], $data);
+
+        $journal->save();
+
+        // update tags:
+        if (isset($data['tags']) && is_array($data['tags'])) {
+            $this->updateTags($journal, $data['tags']);
+        }
+
+        // update meta fields:
+        $result = $journal->save();
+        if ($result) {
+            foreach ($data as $key => $value) {
+                if (in_array($key, $this->validMetaFields)) {
+                    $journal->setMeta($key, $value);
+                    continue;
+                }
+                Log::debug(sprintf('Could not store meta field "%s" with value "%s" for journal #%d', json_encode($key), json_encode($value), $journal->id));
+            }
+
+            return $journal;
+        }
+
+        return $journal;
+    }
+
+    /**
+     * Same as above but for transaction journal with multiple transactions.
      *
      * @param TransactionJournal $journal
-     * @param array              $array
+     * @param array              $data
      *
-     * @return bool
+     * @return TransactionJournal
      */
-    private function saveTags(TransactionJournal $journal, array $array): bool
+    public function updateSplitJournal(TransactionJournal $journal, array $data): TransactionJournal
     {
-        /** @var TagRepositoryInterface $tagRepository */
-        $tagRepository = app(TagRepositoryInterface::class);
+        // update actual journal:
+        $journal->description = $data['journal_description'];
+        $journal->date        = $data['date'];
+        $journal->save();
+        Log::debug(sprintf('Updated split journal #%d', $journal->id));
 
-        foreach ($array as $name) {
-            if (strlen(trim($name)) > 0) {
-                $tag = Tag::firstOrCreateEncrypted(['tag' => $name, 'user_id' => $journal->user_id]);
-                if (!is_null($tag)) {
-                    Log::debug(sprintf('Will try to connect tag #%d to journal #%d.', $tag->id, $journal->id));
-                    $tagRepository->connect($journal, $tag);
+        // unlink all categories:
+        $journal->categories()->detach();
+        $journal->budgets()->detach();
+
+        // update meta fields:
+        $result = $journal->save();
+        if ($result) {
+            foreach ($data as $key => $value) {
+                if (in_array($key, $this->validMetaFields)) {
+                    $journal->setMeta($key, $value);
+                    continue;
                 }
+                Log::debug(sprintf('Could not store meta field "%s" with value "%s" for journal #%d', json_encode($key), json_encode($value), $journal->id));
             }
         }
 
-        return true;
+        // update tags:
+        if (isset($data['tags']) && is_array($data['tags'])) {
+            $this->updateTags($journal, $data['tags']);
+        }
+
+        // delete original transactions, and recreate them.
+        $journal->transactions()->delete();
+
+        // store each transaction.
+        $identifier = 0;
+        Log::debug(sprintf('Count %d transactions in updateSplitJournal()', count($data['transactions'])));
+
+        foreach ($data['transactions'] as $transaction) {
+            Log::debug(sprintf('Split journal update split transaction %d', $identifier));
+            $transaction = $this->appendTransactionData($transaction, $data);
+            $this->storeSplitTransaction($journal, $transaction, $identifier);
+            $identifier++;
+        }
+
+        $journal->save();
+
+        return $journal;
     }
 }
diff --git a/app/Repositories/Journal/JournalRepositoryInterface.php b/app/Repositories/Journal/JournalRepositoryInterface.php
index a25ee4fa23..028f2d52d7 100644
--- a/app/Repositories/Journal/JournalRepositoryInterface.php
+++ b/app/Repositories/Journal/JournalRepositoryInterface.php
@@ -95,4 +95,20 @@ interface JournalRepositoryInterface
      */
     public function store(array $data): TransactionJournal;
 
+    /**
+     * @param TransactionJournal $journal
+     * @param array              $data
+     *
+     * @return TransactionJournal
+     */
+    public function update(TransactionJournal $journal, array $data): TransactionJournal;
+
+    /**
+     * @param TransactionJournal $journal
+     * @param array              $data
+     *
+     * @return TransactionJournal
+     */
+    public function updateSplitJournal(TransactionJournal $journal, array $data): TransactionJournal;
+
 }
diff --git a/app/Repositories/Journal/JournalUpdate.php b/app/Repositories/Journal/JournalUpdate.php
deleted file mode 100644
index ff438369a2..0000000000
--- a/app/Repositories/Journal/JournalUpdate.php
+++ /dev/null
@@ -1,290 +0,0 @@
-user = $user;
-    }
-
-    /**
-     * @param TransactionJournal $journal
-     * @param array              $data
-     *
-     * @return TransactionJournal
-     */
-    public function update(TransactionJournal $journal, array $data): TransactionJournal
-    {
-
-        // update actual journal:
-        $journal->description   = $data['description'];
-        $journal->date          = $data['date'];
-        $accounts               = JournalSupport::storeAccounts($this->user, $journal->transactionType, $data);
-        $data                   = JournalSupport::verifyNativeAmount($data, $accounts);
-        $data['amount']         = strval($data['amount']);
-        $data['foreign_amount'] = is_null($data['foreign_amount']) ? null : strval($data['foreign_amount']);
-
-        // unlink all categories, recreate them:
-        $journal->categories()->detach();
-        $journal->budgets()->detach();
-
-        JournalSupport::storeCategoryWithJournal($journal, $data['category']);
-        JournalSupport::storeBudgetWithJournal($journal, $data['budget_id']);
-
-        // negative because source loses money.
-        $this->updateSourceTransaction($journal, $accounts['source'], $data);
-
-        // positive because destination gets money.
-        $this->updateDestinationTransaction($journal, $accounts['destination'], $data);
-
-        $journal->save();
-
-        // update tags:
-        if (isset($data['tags']) && is_array($data['tags'])) {
-            JournalSupport::updateTags($journal, $data['tags']);
-        }
-
-        // update meta fields:
-        $result = $journal->save();
-        if ($result) {
-            foreach ($data as $key => $value) {
-                if (in_array($key, $this->validMetaFields)) {
-                    $journal->setMeta($key, $value);
-                    continue;
-                }
-                Log::debug(sprintf('Could not store meta field "%s" with value "%s" for journal #%d', json_encode($key), json_encode($value), $journal->id));
-            }
-
-            return $journal;
-        }
-
-        return $journal;
-    }
-
-    /**
-     * Same as above but for transaction journal with multiple transactions.
-     *
-     * @param TransactionJournal $journal
-     * @param array              $data
-     *
-     * @return TransactionJournal
-     */
-    public function updateSplitJournal(TransactionJournal $journal, array $data): TransactionJournal
-    {
-        // update actual journal:
-        $journal->description = $data['journal_description'];
-        $journal->date        = $data['date'];
-        $journal->save();
-        Log::debug(sprintf('Updated split journal #%d', $journal->id));
-
-        // unlink all categories:
-        $journal->categories()->detach();
-        $journal->budgets()->detach();
-
-        // update meta fields:
-        $result = $journal->save();
-        if ($result) {
-            foreach ($data as $key => $value) {
-                if (in_array($key, $this->validMetaFields)) {
-                    $journal->setMeta($key, $value);
-                    continue;
-                }
-                Log::debug(sprintf('Could not store meta field "%s" with value "%s" for journal #%d', json_encode($key), json_encode($value), $journal->id));
-            }
-        }
-
-
-        // update tags:
-        if (isset($data['tags']) && is_array($data['tags'])) {
-            JournalSupport::updateTags($journal, $data['tags']);
-        }
-
-        // delete original transactions, and recreate them.
-        $journal->transactions()->delete();
-
-        // store each transaction.
-        $identifier = 0;
-        Log::debug(sprintf('Count %d transactions in updateSplitJournal()', count($data['transactions'])));
-
-        foreach ($data['transactions'] as $transaction) {
-            Log::debug(sprintf('Split journal update split transaction %d', $identifier));
-            $transaction = $this->appendTransactionData($transaction, $data);
-            $this->storeSplitTransaction($journal, $transaction, $identifier);
-            $identifier++;
-        }
-
-        $journal->save();
-
-        return $journal;
-    }
-
-    /**
-     * When the user edits a split journal, each line is missing crucial data:
-     *
-     * - Withdrawal lines are missing the source account ID
-     * - Deposit lines are missing the destination account ID
-     * - Transfers are missing both.
-     *
-     * We need to append the array.
-     *
-     * @param array $transaction
-     * @param array $data
-     *
-     * @return array
-     */
-    private function appendTransactionData(array $transaction, array $data): array
-    {
-        switch ($data['what']) {
-            case strtolower(TransactionType::TRANSFER):
-            case strtolower(TransactionType::WITHDRAWAL):
-                $transaction['source_account_id'] = intval($data['journal_source_account_id']);
-                break;
-        }
-
-        switch ($data['what']) {
-            case strtolower(TransactionType::TRANSFER):
-            case strtolower(TransactionType::DEPOSIT):
-                $transaction['destination_account_id'] = intval($data['journal_destination_account_id']);
-                break;
-        }
-
-        return $transaction;
-    }
-
-    /**
-     * @param TransactionJournal $journal
-     * @param array              $transaction
-     * @param int                $identifier
-     *
-     * @return Collection
-     */
-    private function storeSplitTransaction(TransactionJournal $journal, array $transaction, int $identifier): Collection
-    {
-        // store source and destination accounts (depends on type)
-        $accounts = JournalSupport::storeAccounts($this->user, $journal->transactionType, $transaction);
-
-        // store transaction one way:
-        $amount        = bcmul(strval($transaction['amount']), '-1');
-        $foreignAmount = is_null($transaction['foreign_amount']) ? null : bcmul(strval($transaction['foreign_amount']), '-1');
-        $one           = JournalSupport::storeTransaction(
-            [
-                'journal'                 => $journal,
-                'account'                 => $accounts['source'],
-                'amount'                  => $amount,
-                'transaction_currency_id' => $transaction['transaction_currency_id'],
-                'foreign_amount'          => $foreignAmount,
-                'foreign_currency_id'     => $transaction['foreign_currency_id'],
-                'description'             => $transaction['description'],
-                'category'                => null,
-                'budget'                  => null,
-                'identifier'              => $identifier,
-            ]
-        );
-        JournalSupport::storeCategoryWithTransaction($one, $transaction['category']);
-        JournalSupport::storeBudgetWithTransaction($one, $transaction['budget_id']);
-
-        // and the other way:
-        $amount        = strval($transaction['amount']);
-        $foreignAmount = is_null($transaction['foreign_amount']) ? null : strval($transaction['foreign_amount']);
-        $two           = JournalSupport::storeTransaction(
-            [
-                'journal'                 => $journal,
-                'account'                 => $accounts['destination'],
-                'amount'                  => $amount,
-                'transaction_currency_id' => $transaction['transaction_currency_id'],
-                'foreign_amount'          => $foreignAmount,
-                'foreign_currency_id'     => $transaction['foreign_currency_id'],
-                'description'             => $transaction['description'],
-                'category'                => null,
-                'budget'                  => null,
-                'identifier'              => $identifier,
-            ]
-        );
-        JournalSupport::storeCategoryWithTransaction($two, $transaction['category']);
-        JournalSupport::storeBudgetWithTransaction($two, $transaction['budget_id']);
-
-        return new Collection([$one, $two]);
-    }
-
-    /**
-     * @param TransactionJournal $journal
-     * @param Account            $account
-     * @param array              $data
-     *
-     * @throws FireflyException
-     */
-    private function updateDestinationTransaction(TransactionJournal $journal, Account $account, array $data)
-    {
-        $set = $journal->transactions()->where('amount', '>', 0)->get();
-        if ($set->count() != 1) {
-            throw new FireflyException(sprintf('Journal #%d has %d transactions with an amount more than zero.', $journal->id, $set->count()));
-        }
-        /** @var Transaction $transaction */
-        $transaction                          = $set->first();
-        $transaction->amount                  = app('steam')->positive($data['amount']);
-        $transaction->transaction_currency_id = $data['currency_id'];
-        $transaction->foreign_amount          = is_null($data['foreign_amount']) ? null : app('steam')->positive($data['foreign_amount']);
-        $transaction->foreign_currency_id     = $data['foreign_currency_id'];
-        $transaction->account_id              = $account->id;
-        $transaction->save();
-
-    }
-
-    /**
-     * @param TransactionJournal $journal
-     * @param Account            $account
-     * @param array              $data
-     *
-     * @throws FireflyException
-     */
-    private function updateSourceTransaction(TransactionJournal $journal, Account $account, array $data)
-    {
-        // should be one:
-        $set = $journal->transactions()->where('amount', '<', 0)->get();
-        if ($set->count() != 1) {
-            throw new FireflyException(sprintf('Journal #%d has %d transactions with an amount more than zero.', $journal->id, $set->count()));
-        }
-        /** @var Transaction $transaction */
-        $transaction                          = $set->first();
-        $transaction->amount                  = bcmul(app('steam')->positive($data['amount']), '-1');
-        $transaction->transaction_currency_id = $data['currency_id'];
-        $transaction->foreign_amount          = is_null($data['foreign_amount']) ? null : bcmul(app('steam')->positive($data['foreign_amount']), '-1');
-        $transaction->foreign_currency_id     = $data['foreign_currency_id'];
-        $transaction->account_id              = $account->id;
-        $transaction->save();
-    }
-
-}
\ No newline at end of file
diff --git a/app/Repositories/Journal/JournalUpdateInterface.php b/app/Repositories/Journal/JournalUpdateInterface.php
deleted file mode 100644
index 21f6f80c85..0000000000
--- a/app/Repositories/Journal/JournalUpdateInterface.php
+++ /dev/null
@@ -1,44 +0,0 @@
- null,
@@ -50,11 +46,11 @@ class JournalSupport
         Log::debug(sprintf('Going to store accounts for type %s', $type->type));
         switch ($type->type) {
             case TransactionType::WITHDRAWAL:
-                $accounts = self::storeWithdrawalAccounts($user, $data);
+                $accounts = $this->storeWithdrawalAccounts($user, $data);
                 break;
 
             case TransactionType::DEPOSIT:
-                $accounts = self::storeDepositAccounts($user, $data);
+                $accounts = $this->storeDepositAccounts($user, $data);
 
                 break;
             case TransactionType::TRANSFER:
@@ -84,7 +80,7 @@ class JournalSupport
      * @param TransactionJournal $journal
      * @param int                $budgetId
      */
-    public static function storeBudgetWithJournal(TransactionJournal $journal, int $budgetId)
+    protected function storeBudgetWithJournal(TransactionJournal $journal, int $budgetId)
     {
         if (intval($budgetId) > 0 && $journal->transactionType->type === TransactionType::WITHDRAWAL) {
             /** @var \FireflyIII\Models\Budget $budget */
@@ -93,24 +89,11 @@ class JournalSupport
         }
     }
 
-    /**
-     * @param Transaction $transaction
-     * @param int         $budgetId
-     */
-    public static function storeBudgetWithTransaction(Transaction $transaction, int $budgetId)
-    {
-        if (intval($budgetId) > 0 && $transaction->transactionJournal->transactionType->type !== TransactionType::TRANSFER) {
-            /** @var \FireflyIII\Models\Budget $budget */
-            $budget = Budget::find($budgetId);
-            $transaction->budgets()->save($budget);
-        }
-    }
-
     /**
      * @param TransactionJournal $journal
      * @param string             $category
      */
-    public static function storeCategoryWithJournal(TransactionJournal $journal, string $category)
+    protected function storeCategoryWithJournal(TransactionJournal $journal, string $category)
     {
         if (strlen($category) > 0) {
             $category = Category::firstOrCreateEncrypted(['name' => $category, 'user_id' => $journal->user_id]);
@@ -118,25 +101,13 @@ class JournalSupport
         }
     }
 
-    /**
-     * @param Transaction $transaction
-     * @param string      $category
-     */
-    public static function storeCategoryWithTransaction(Transaction $transaction, string $category)
-    {
-        if (strlen($category) > 0) {
-            $category = Category::firstOrCreateEncrypted(['name' => $category, 'user_id' => $transaction->transactionJournal->user_id]);
-            $transaction->categories()->save($category);
-        }
-    }
-
     /**
      * @param User  $user
      * @param array $data
      *
      * @return array
      */
-    public static function storeDepositAccounts(User $user, array $data): array
+    protected function storeDepositAccounts(User $user, array $data): array
     {
         Log::debug('Now in storeDepositAccounts().');
         $destinationAccount = Account::where('user_id', $user->id)->where('id', $data['destination_account_id'])->first(['accounts.*']);
@@ -170,56 +141,13 @@ class JournalSupport
         ];
     }
 
-    /**
-     * @param array $data
-     *
-     * @return Transaction
-     */
-    public static function storeTransaction(array $data): Transaction
-    {
-        $fields = [
-            'transaction_journal_id'  => $data['journal']->id,
-            'account_id'              => $data['account']->id,
-            'amount'                  => $data['amount'],
-            'foreign_amount'          => $data['foreign_amount'],
-            'transaction_currency_id' => $data['transaction_currency_id'],
-            'foreign_currency_id'     => $data['foreign_currency_id'],
-            'description'             => $data['description'],
-            'identifier'              => $data['identifier'],
-        ];
-
-
-        if (is_null($data['foreign_currency_id'])) {
-            unset($fields['foreign_currency_id']);
-        }
-        if (is_null($data['foreign_amount'])) {
-            unset($fields['foreign_amount']);
-        }
-
-        /** @var Transaction $transaction */
-        $transaction = Transaction::create($fields);
-
-        Log::debug(sprintf('Transaction stored with ID: %s', $transaction->id));
-
-        if (!is_null($data['category'])) {
-            $transaction->categories()->save($data['category']);
-        }
-
-        if (!is_null($data['budget'])) {
-            $transaction->categories()->save($data['budget']);
-        }
-
-        return $transaction;
-
-    }
-
     /**
      * @param User  $user
      * @param array $data
      *
      * @return array
      */
-    public static function storeWithdrawalAccounts(User $user, array $data): array
+    protected function storeWithdrawalAccounts(User $user, array $data): array
     {
         Log::debug('Now in storeWithdrawalAccounts().');
         $sourceAccount = Account::where('user_id', $user->id)->where('id', $data['source_account_id'])->first(['accounts.*']);
@@ -256,49 +184,6 @@ class JournalSupport
         ];
     }
 
-    /**
-     * @param TransactionJournal $journal
-     * @param array              $array
-     *
-     * @return bool
-     */
-    public static function updateTags(TransactionJournal $journal, array $array): bool
-    {
-        // create tag repository
-        /** @var TagRepositoryInterface $tagRepository */
-        $tagRepository = app(TagRepositoryInterface::class);
-
-
-        // find or create all tags:
-        $tags = [];
-        $ids  = [];
-        foreach ($array as $name) {
-            if (strlen(trim($name)) > 0) {
-                $tag    = Tag::firstOrCreateEncrypted(['tag' => $name, 'user_id' => $journal->user_id]);
-                $tags[] = $tag;
-                $ids[]  = $tag->id;
-            }
-        }
-
-        // delete all tags connected to journal not in this array:
-        if (count($ids) > 0) {
-            DB::table('tag_transaction_journal')->where('transaction_journal_id', $journal->id)->whereNotIn('tag_id', $ids)->delete();
-        }
-        // if count is zero, delete them all:
-        if (count($ids) == 0) {
-            DB::table('tag_transaction_journal')->where('transaction_journal_id', $journal->id)->delete();
-        }
-
-        // connect each tag to journal (if not yet connected):
-        /** @var Tag $tag */
-        foreach ($tags as $tag) {
-            Log::debug(sprintf('Will try to connect tag #%d to journal #%d.', $tag->id, $journal->id));
-            $tagRepository->connect($journal, $tag);
-        }
-
-        return true;
-    }
-
     /**
      * This method checks the data array and the given accounts to verify that the native amount, currency
      * and possible the foreign currency and amount are properly saved.
@@ -309,7 +194,7 @@ class JournalSupport
      * @return array
      * @throws FireflyException
      */
-    public static function verifyNativeAmount(array $data, array $accounts): array
+    protected function verifyNativeAmount(array $data, array $accounts): array
     {
         /** @var TransactionType $transactionType */
         $transactionType             = TransactionType::where('type', ucfirst($data['what']))->first();
diff --git a/app/Repositories/Journal/UpdateJournalsTrait.php b/app/Repositories/Journal/UpdateJournalsTrait.php
new file mode 100644
index 0000000000..8e249d5bff
--- /dev/null
+++ b/app/Repositories/Journal/UpdateJournalsTrait.php
@@ -0,0 +1,156 @@
+transactions()->where('amount', '>', 0)->get();
+        if ($set->count() != 1) {
+            throw new FireflyException(sprintf('Journal #%d has %d transactions with an amount more than zero.', $journal->id, $set->count()));
+        }
+        /** @var Transaction $transaction */
+        $transaction                          = $set->first();
+        $transaction->amount                  = app('steam')->positive($data['amount']);
+        $transaction->transaction_currency_id = $data['currency_id'];
+        $transaction->foreign_amount          = is_null($data['foreign_amount']) ? null : app('steam')->positive($data['foreign_amount']);
+        $transaction->foreign_currency_id     = $data['foreign_currency_id'];
+        $transaction->account_id              = $account->id;
+        $transaction->save();
+
+    }
+
+    /**
+     * @param TransactionJournal $journal
+     * @param Account            $account
+     * @param array              $data
+     *
+     * @throws FireflyException
+     */
+    protected function updateSourceTransaction(TransactionJournal $journal, Account $account, array $data)
+    {
+        // should be one:
+        $set = $journal->transactions()->where('amount', '<', 0)->get();
+        if ($set->count() != 1) {
+            throw new FireflyException(sprintf('Journal #%d has %d transactions with an amount more than zero.', $journal->id, $set->count()));
+        }
+        /** @var Transaction $transaction */
+        $transaction                          = $set->first();
+        $transaction->amount                  = bcmul(app('steam')->positive($data['amount']), '-1');
+        $transaction->transaction_currency_id = $data['currency_id'];
+        $transaction->foreign_amount          = is_null($data['foreign_amount']) ? null : bcmul(app('steam')->positive($data['foreign_amount']), '-1');
+        $transaction->foreign_currency_id     = $data['foreign_currency_id'];
+        $transaction->account_id              = $account->id;
+        $transaction->save();
+    }
+
+    /**
+     * @param TransactionJournal $journal
+     * @param array              $array
+     *
+     * @return bool
+     */
+    protected function updateTags(TransactionJournal $journal, array $array): bool
+    {
+        // create tag repository
+        /** @var TagRepositoryInterface $tagRepository */
+        $tagRepository = app(TagRepositoryInterface::class);
+
+
+        // find or create all tags:
+        $tags = [];
+        $ids  = [];
+        foreach ($array as $name) {
+            if (strlen(trim($name)) > 0) {
+                $tag    = Tag::firstOrCreateEncrypted(['tag' => $name, 'user_id' => $journal->user_id]);
+                $tags[] = $tag;
+                $ids[]  = $tag->id;
+            }
+        }
+
+        // delete all tags connected to journal not in this array:
+        if (count($ids) > 0) {
+            DB::table('tag_transaction_journal')->where('transaction_journal_id', $journal->id)->whereNotIn('tag_id', $ids)->delete();
+        }
+        // if count is zero, delete them all:
+        if (count($ids) == 0) {
+            DB::table('tag_transaction_journal')->where('transaction_journal_id', $journal->id)->delete();
+        }
+
+        // connect each tag to journal (if not yet connected):
+        /** @var Tag $tag */
+        foreach ($tags as $tag) {
+            Log::debug(sprintf('Will try to connect tag #%d to journal #%d.', $tag->id, $journal->id));
+            $tagRepository->connect($journal, $tag);
+        }
+
+        return true;
+    }
+}
\ No newline at end of file

From 8bbd3063ec0eea2e77a9bd3622825735667cf49e Mon Sep 17 00:00:00 2001
From: James Cole 
Date: Wed, 7 Jun 2017 11:13:04 +0200
Subject: [PATCH 036/126] Move code around for simplicity and fix tests.

---
 app/Handlers/Events/UserEventHandler.php      |   4 +-
 app/Import/Setup/CsvSetup.php                 | 222 +---------------
 app/Mail/RegisteredUser.php                   |   8 +-
 app/Mail/RequestedNewPassword.php             |  12 +-
 app/Support/Import/CsvImportSupportTrait.php  | 242 ++++++++++++++++++
 app/User.php                                  |   4 +-
 resources/views/emails/footer-html.twig       |   2 +-
 resources/views/emails/footer-text.twig       |   2 +-
 routes/web.php                                |   2 +-
 .../Controllers/BudgetControllerTest.php      |   6 +-
 .../Transaction/MassControllerTest.php        |   6 +-
 .../Transaction/SingleControllerTest.php      |   4 +-
 .../Transaction/SplitControllerTest.php       |   4 +-
 .../Handlers/Events/UserEventHandlerTest.php  |   4 +-
 14 files changed, 271 insertions(+), 251 deletions(-)
 create mode 100644 app/Support/Import/CsvImportSupportTrait.php

diff --git a/app/Handlers/Events/UserEventHandler.php b/app/Handlers/Events/UserEventHandler.php
index 0259c403e5..8c7eb4a90c 100644
--- a/app/Handlers/Events/UserEventHandler.php
+++ b/app/Handlers/Events/UserEventHandler.php
@@ -97,12 +97,12 @@ class UserEventHandler
         }
         // get the email address
         $email     = $event->user->email;
-        $address   = route('index');
+        $uri       = route('index');
         $ipAddress = $event->ipAddress;
 
         // send email.
         try {
-            Mail::to($email)->send(new RegisteredUserMail($address, $ipAddress));
+            Mail::to($email)->send(new RegisteredUserMail($uri, $ipAddress));
             // @codeCoverageIgnoreStart
         } catch (Swift_TransportException $e) {
             Log::error($e->getMessage());
diff --git a/app/Import/Setup/CsvSetup.php b/app/Import/Setup/CsvSetup.php
index 51ccedb338..e3888e0030 100644
--- a/app/Import/Setup/CsvSetup.php
+++ b/app/Import/Setup/CsvSetup.php
@@ -16,15 +16,12 @@ namespace FireflyIII\Import\Setup;
 
 use ExpandedForm;
 use FireflyIII\Exceptions\FireflyException;
-use FireflyIII\Import\Mapper\MapperInterface;
-use FireflyIII\Import\MapperPreProcess\PreProcessorInterface;
-use FireflyIII\Import\Specifics\SpecificInterface;
 use FireflyIII\Models\Account;
 use FireflyIII\Models\AccountType;
 use FireflyIII\Models\ImportJob;
 use FireflyIII\Repositories\Account\AccountRepositoryInterface;
+use FireflyIII\Support\Import\CsvImportSupportTrait;
 use Illuminate\Http\Request;
-use League\Csv\Reader;
 use Log;
 use Symfony\Component\HttpFoundation\FileBag;
 
@@ -35,6 +32,7 @@ use Symfony\Component\HttpFoundation\FileBag;
  */
 class CsvSetup implements SetupInterface
 {
+    use CsvImportSupportTrait;
     /** @var  Account */
     public $defaultImportAccount;
     /** @var  ImportJob */
@@ -286,220 +284,4 @@ class CsvSetup implements SetupInterface
             $this->job->save();
         }
     }
-
-    /**
-     * @return bool
-     */
-    private function doColumnMapping(): bool
-    {
-        $mapArray = $this->job->configuration['column-do-mapping'] ?? [];
-        $doMap    = false;
-        foreach ($mapArray as $value) {
-            if ($value === true) {
-                $doMap = true;
-                break;
-            }
-        }
-
-        return $this->job->configuration['column-mapping-complete'] === false && $doMap;
-    }
-
-    /**
-     * @return bool
-     */
-    private function doColumnRoles(): bool
-    {
-        return $this->job->configuration['column-roles-complete'] === false;
-    }
-
-    /**
-     * @return array
-     * @throws FireflyException
-     */
-    private function getDataForColumnMapping(): array
-    {
-        $config  = $this->job->configuration;
-        $data    = [];
-        $indexes = [];
-
-        foreach ($config['column-do-mapping'] as $index => $mustBeMapped) {
-            if ($mustBeMapped) {
-
-                $column = $config['column-roles'][$index] ?? '_ignore';
-
-                // is valid column?
-                $validColumns = array_keys(config('csv.import_roles'));
-                if (!in_array($column, $validColumns)) {
-                    throw new FireflyException(sprintf('"%s" is not a valid column.', $column));
-                }
-
-                $canBeMapped   = config('csv.import_roles.' . $column . '.mappable');
-                $preProcessMap = config('csv.import_roles.' . $column . '.pre-process-map');
-                if ($canBeMapped) {
-                    $mapperClass = config('csv.import_roles.' . $column . '.mapper');
-                    $mapperName  = sprintf('\\FireflyIII\\Import\Mapper\\%s', $mapperClass);
-                    /** @var MapperInterface $mapper */
-                    $mapper       = new $mapperName;
-                    $indexes[]    = $index;
-                    $data[$index] = [
-                        'name'          => $column,
-                        'mapper'        => $mapperName,
-                        'index'         => $index,
-                        'options'       => $mapper->getMap(),
-                        'preProcessMap' => null,
-                        'values'        => [],
-                    ];
-                    if ($preProcessMap) {
-                        $preClass                      = sprintf(
-                            '\\FireflyIII\\Import\\MapperPreProcess\\%s',
-                            config('csv.import_roles.' . $column . '.pre-process-mapper')
-                        );
-                        $data[$index]['preProcessMap'] = $preClass;
-                    }
-                }
-
-            }
-        }
-
-        // in order to actually map we also need all possible values from the CSV file.
-        $content = $this->job->uploadFileContents();
-        /** @var Reader $reader */
-        $reader = Reader::createFromString($content);
-        $reader->setDelimiter($config['delimiter']);
-        $results        = $reader->fetch();
-        $validSpecifics = array_keys(config('csv.import_specifics'));
-
-        foreach ($results as $rowIndex => $row) {
-
-            // skip first row?
-            if ($rowIndex === 0 && $config['has-headers']) {
-                continue;
-            }
-
-            // run specifics here:
-            // and this is the point where the specifix go to work.
-            foreach ($config['specifics'] as $name => $enabled) {
-
-                if (!in_array($name, $validSpecifics)) {
-                    throw new FireflyException(sprintf('"%s" is not a valid class name', $name));
-                }
-                $class = config('csv.import_specifics.' . $name);
-                /** @var SpecificInterface $specific */
-                $specific = app($class);
-
-                // it returns the row, possibly modified:
-                $row = $specific->run($row);
-            }
-
-            //do something here
-            foreach ($indexes as $index) { // this is simply 1, 2, 3, etc.
-                if (!isset($row[$index])) {
-                    // don't really know how to handle this. Just skip, for now.
-                    continue;
-                }
-                $value = $row[$index];
-                if (strlen($value) > 0) {
-
-                    // we can do some preprocessing here,
-                    // which is exclusively to fix the tags:
-                    if (!is_null($data[$index]['preProcessMap'])) {
-                        /** @var PreProcessorInterface $preProcessor */
-                        $preProcessor           = app($data[$index]['preProcessMap']);
-                        $result                 = $preProcessor->run($value);
-                        $data[$index]['values'] = array_merge($data[$index]['values'], $result);
-
-                        Log::debug($rowIndex . ':' . $index . 'Value before preprocessor', ['value' => $value]);
-                        Log::debug($rowIndex . ':' . $index . 'Value after preprocessor', ['value-new' => $result]);
-                        Log::debug($rowIndex . ':' . $index . 'Value after joining', ['value-complete' => $data[$index]['values']]);
-
-
-                        continue;
-                    }
-
-                    $data[$index]['values'][] = $value;
-                }
-            }
-        }
-        foreach ($data as $index => $entry) {
-            $data[$index]['values'] = array_unique($data[$index]['values']);
-        }
-
-        return $data;
-    }
-
-    /**
-     * This method collects the data that will enable a user to choose column content.
-     *
-     * @return array
-     */
-    private function getDataForColumnRoles(): array
-    {
-        Log::debug('Now in getDataForColumnRoles()');
-        $config = $this->job->configuration;
-        $data   = [
-            'columns'       => [],
-            'columnCount'   => 0,
-            'columnHeaders' => [],
-        ];
-
-        // show user column role configuration.
-        $content = $this->job->uploadFileContents();
-
-        // create CSV reader.
-        $reader = Reader::createFromString($content);
-        $reader->setDelimiter($config['delimiter']);
-        $start  = $config['has-headers'] ? 1 : 0;
-        $end    = $start + config('csv.example_rows');
-        $header = [];
-        if ($config['has-headers']) {
-            $header = $reader->fetchOne(0);
-        }
-
-
-        // collect example data in $data['columns']
-        Log::debug(sprintf('While %s is smaller than %d', $start, $end));
-        while ($start < $end) {
-            $row = $reader->fetchOne($start);
-            Log::debug(sprintf('Row %d has %d columns', $start, count($row)));
-            // run specifics here:
-            // and this is the point where the specifix go to work.
-            foreach ($config['specifics'] as $name => $enabled) {
-                /** @var SpecificInterface $specific */
-                $specific = app('FireflyIII\Import\Specifics\\' . $name);
-                Log::debug(sprintf('Will now apply specific "%s" to row %d.', $name, $start));
-                // it returns the row, possibly modified:
-                $row = $specific->run($row);
-            }
-
-            foreach ($row as $index => $value) {
-                $value                         = trim($value);
-                $data['columnHeaders'][$index] = $header[$index] ?? '';
-                if (strlen($value) > 0) {
-                    $data['columns'][$index][] = $value;
-                }
-            }
-            $start++;
-            $data['columnCount'] = count($row) > $data['columnCount'] ? count($row) : $data['columnCount'];
-        }
-
-        // make unique example data
-        foreach ($data['columns'] as $index => $values) {
-            $data['columns'][$index] = array_unique($values);
-        }
-
-        $data['set_roles'] = [];
-        // collect possible column roles:
-        $data['available_roles'] = [];
-        foreach (array_keys(config('csv.import_roles')) as $role) {
-            $data['available_roles'][$role] = trans('csv.column_' . $role);
-        }
-
-        $config['column-count']   = $data['columnCount'];
-        $this->job->configuration = $config;
-        $this->job->save();
-
-        return $data;
-
-
-    }
 }
diff --git a/app/Mail/RegisteredUser.php b/app/Mail/RegisteredUser.php
index a693fb13e1..d988506393 100644
--- a/app/Mail/RegisteredUser.php
+++ b/app/Mail/RegisteredUser.php
@@ -12,18 +12,18 @@ class RegisteredUser extends Mailable
     /** @var  string */
     public $address;
     /** @var  string */
-    public $userIp;
+    public $ipAddress;
 
     /**
      * Create a new message instance.
      *
      * @param string $address
-     * @param string $userIp
+     * @param string $ipAddress
      */
-    public function __construct(string $address, string $userIp)
+    public function __construct(string $address, string $ipAddress)
     {
         $this->address = $address;
-        $this->userIp  = $userIp;
+        $this->ipAddress  = $ipAddress;
     }
 
     /**
diff --git a/app/Mail/RequestedNewPassword.php b/app/Mail/RequestedNewPassword.php
index cdda5b9858..bd2d9e90b0 100644
--- a/app/Mail/RequestedNewPassword.php
+++ b/app/Mail/RequestedNewPassword.php
@@ -10,20 +10,20 @@ class RequestedNewPassword extends Mailable
 {
     use Queueable, SerializesModels;
     /** @var  string */
-    public $url;
+    public $ipAddress;
     /** @var  string */
-    public $userIp;
+    public $url;
 
     /**
      * RequestedNewPassword constructor.
      *
      * @param string $url
-     * @param string $userIp
+     * @param string $ipAddress
      */
-    public function __construct(string $url, string $userIp)
+    public function __construct(string $url, string $ipAddress)
     {
-        $this->url    = $url;
-        $this->userIp = $userIp;
+        $this->url       = $url;
+        $this->ipAddress = $ipAddress;
     }
 
     /**
diff --git a/app/Support/Import/CsvImportSupportTrait.php b/app/Support/Import/CsvImportSupportTrait.php
new file mode 100644
index 0000000000..efee16704a
--- /dev/null
+++ b/app/Support/Import/CsvImportSupportTrait.php
@@ -0,0 +1,242 @@
+job->configuration['column-do-mapping'] ?? [];
+        $doMap    = false;
+        foreach ($mapArray as $value) {
+            if ($value === true) {
+                $doMap = true;
+                break;
+            }
+        }
+
+        return $this->job->configuration['column-mapping-complete'] === false && $doMap;
+    }
+
+    /**
+     * @return bool
+     */
+    protected function doColumnRoles(): bool
+    {
+        return $this->job->configuration['column-roles-complete'] === false;
+    }
+
+    /**
+     * @return array
+     * @throws FireflyException
+     */
+    protected function getDataForColumnMapping(): array
+    {
+        $config  = $this->job->configuration;
+        $data    = [];
+        $indexes = [];
+
+        foreach ($config['column-do-mapping'] as $index => $mustBeMapped) {
+            if ($mustBeMapped) {
+
+                $column = $config['column-roles'][$index] ?? '_ignore';
+
+                // is valid column?
+                $validColumns = array_keys(config('csv.import_roles'));
+                if (!in_array($column, $validColumns)) {
+                    throw new FireflyException(sprintf('"%s" is not a valid column.', $column));
+                }
+
+                $canBeMapped   = config('csv.import_roles.' . $column . '.mappable');
+                $preProcessMap = config('csv.import_roles.' . $column . '.pre-process-map');
+                if ($canBeMapped) {
+                    $mapperClass = config('csv.import_roles.' . $column . '.mapper');
+                    $mapperName  = sprintf('\\FireflyIII\\Import\Mapper\\%s', $mapperClass);
+                    /** @var MapperInterface $mapper */
+                    $mapper       = new $mapperName;
+                    $indexes[]    = $index;
+                    $data[$index] = [
+                        'name'          => $column,
+                        'mapper'        => $mapperName,
+                        'index'         => $index,
+                        'options'       => $mapper->getMap(),
+                        'preProcessMap' => null,
+                        'values'        => [],
+                    ];
+                    if ($preProcessMap) {
+                        $preClass                      = sprintf(
+                            '\\FireflyIII\\Import\\MapperPreProcess\\%s',
+                            config('csv.import_roles.' . $column . '.pre-process-mapper')
+                        );
+                        $data[$index]['preProcessMap'] = $preClass;
+                    }
+                }
+
+            }
+        }
+
+        // in order to actually map we also need all possible values from the CSV file.
+        $content = $this->job->uploadFileContents();
+        /** @var Reader $reader */
+        $reader = Reader::createFromString($content);
+        $reader->setDelimiter($config['delimiter']);
+        $results        = $reader->fetch();
+        $validSpecifics = array_keys(config('csv.import_specifics'));
+
+        foreach ($results as $rowIndex => $row) {
+
+            // skip first row?
+            if ($rowIndex === 0 && $config['has-headers']) {
+                continue;
+            }
+
+            // run specifics here:
+            // and this is the point where the specifix go to work.
+            foreach ($config['specifics'] as $name => $enabled) {
+
+                if (!in_array($name, $validSpecifics)) {
+                    throw new FireflyException(sprintf('"%s" is not a valid class name', $name));
+                }
+                $class = config('csv.import_specifics.' . $name);
+                /** @var SpecificInterface $specific */
+                $specific = app($class);
+
+                // it returns the row, possibly modified:
+                $row = $specific->run($row);
+            }
+
+            //do something here
+            foreach ($indexes as $index) { // this is simply 1, 2, 3, etc.
+                if (!isset($row[$index])) {
+                    // don't really know how to handle this. Just skip, for now.
+                    continue;
+                }
+                $value = $row[$index];
+                if (strlen($value) > 0) {
+
+                    // we can do some preprocessing here,
+                    // which is exclusively to fix the tags:
+                    if (!is_null($data[$index]['preProcessMap'])) {
+                        /** @var PreProcessorInterface $preProcessor */
+                        $preProcessor           = app($data[$index]['preProcessMap']);
+                        $result                 = $preProcessor->run($value);
+                        $data[$index]['values'] = array_merge($data[$index]['values'], $result);
+
+                        Log::debug($rowIndex . ':' . $index . 'Value before preprocessor', ['value' => $value]);
+                        Log::debug($rowIndex . ':' . $index . 'Value after preprocessor', ['value-new' => $result]);
+                        Log::debug($rowIndex . ':' . $index . 'Value after joining', ['value-complete' => $data[$index]['values']]);
+
+
+                        continue;
+                    }
+
+                    $data[$index]['values'][] = $value;
+                }
+            }
+        }
+        foreach ($data as $index => $entry) {
+            $data[$index]['values'] = array_unique($data[$index]['values']);
+        }
+
+        return $data;
+    }
+
+    /**
+     * This method collects the data that will enable a user to choose column content.
+     *
+     * @return array
+     */
+    protected function getDataForColumnRoles(): array
+    {
+        Log::debug('Now in getDataForColumnRoles()');
+        $config = $this->job->configuration;
+        $data   = [
+            'columns'       => [],
+            'columnCount'   => 0,
+            'columnHeaders' => [],
+        ];
+
+        // show user column role configuration.
+        $content = $this->job->uploadFileContents();
+
+        // create CSV reader.
+        $reader = Reader::createFromString($content);
+        $reader->setDelimiter($config['delimiter']);
+        $start  = $config['has-headers'] ? 1 : 0;
+        $end    = $start + config('csv.example_rows');
+        $header = [];
+        if ($config['has-headers']) {
+            $header = $reader->fetchOne(0);
+        }
+
+
+        // collect example data in $data['columns']
+        Log::debug(sprintf('While %s is smaller than %d', $start, $end));
+        while ($start < $end) {
+            $row = $reader->fetchOne($start);
+            Log::debug(sprintf('Row %d has %d columns', $start, count($row)));
+            // run specifics here:
+            // and this is the point where the specifix go to work.
+            foreach ($config['specifics'] as $name => $enabled) {
+                /** @var SpecificInterface $specific */
+                $specific = app('FireflyIII\Import\Specifics\\' . $name);
+                Log::debug(sprintf('Will now apply specific "%s" to row %d.', $name, $start));
+                // it returns the row, possibly modified:
+                $row = $specific->run($row);
+            }
+
+            foreach ($row as $index => $value) {
+                $value                         = trim($value);
+                $data['columnHeaders'][$index] = $header[$index] ?? '';
+                if (strlen($value) > 0) {
+                    $data['columns'][$index][] = $value;
+                }
+            }
+            $start++;
+            $data['columnCount'] = count($row) > $data['columnCount'] ? count($row) : $data['columnCount'];
+        }
+
+        // make unique example data
+        foreach ($data['columns'] as $index => $values) {
+            $data['columns'][$index] = array_unique($values);
+        }
+
+        $data['set_roles'] = [];
+        // collect possible column roles:
+        $data['available_roles'] = [];
+        foreach (array_keys(config('csv.import_roles')) as $role) {
+            $data['available_roles'][$role] = trans('csv.column_' . $role);
+        }
+
+        $config['column-count']   = $data['columnCount'];
+        $this->job->configuration = $config;
+        $this->job->save();
+
+        return $data;
+    }
+}
\ No newline at end of file
diff --git a/app/User.php b/app/User.php
index f5ee28f920..3c045f2412 100644
--- a/app/User.php
+++ b/app/User.php
@@ -214,9 +214,9 @@ class User extends Authenticatable
      */
     public function sendPasswordResetNotification($token)
     {
-        $ip = Request::ip();
+        $ipAddress = Request::ip();
 
-        event(new RequestedNewPassword($this, $token, $ip));
+        event(new RequestedNewPassword($this, $token, $ipAddress));
     }
 
     /**
diff --git a/resources/views/emails/footer-html.twig b/resources/views/emails/footer-html.twig
index 935128c2aa..6e2ca37007 100644
--- a/resources/views/emails/footer-html.twig
+++ b/resources/views/emails/footer-html.twig
@@ -6,7 +6,7 @@
 

- PS: This message was sent because a request from IP {{ ip }} {{ userIp }} triggered it. + PS: This message was sent because a request from IP {{ ip }}{{ userIp }}{{ ipAddress }} triggered it.

diff --git a/resources/views/emails/footer-text.twig b/resources/views/emails/footer-text.twig index 2e4d10c979..e2ec3ff9d4 100644 --- a/resources/views/emails/footer-text.twig +++ b/resources/views/emails/footer-text.twig @@ -3,4 +3,4 @@ Beep boop, The Firefly III Mail Robot -PS: This message was sent because a request from IP {{ ip }} {{ userIp }} triggered it. +PS: This message was sent because a request from IP {{ ip }}{{ userIp }}{{ ipAddress }} triggered it. diff --git a/routes/web.php b/routes/web.php index d8e969e4ac..9782f9d6c6 100755 --- a/routes/web.php +++ b/routes/web.php @@ -134,7 +134,6 @@ Route::group( */ Route::group( ['middleware' => 'user-full-auth', 'prefix' => 'budgets', 'as' => 'budgets.'], function () { - Route::get('{moment?}', ['uses' => 'BudgetController@index', 'as' => 'index']); Route::get('income', ['uses' => 'BudgetController@updateIncome', 'as' => 'income']); Route::get('create', ['uses' => 'BudgetController@create', 'as' => 'create']); Route::get('edit/{budget}', ['uses' => 'BudgetController@edit', 'as' => 'edit']); @@ -142,6 +141,7 @@ Route::group( Route::get('show/{budget}', ['uses' => 'BudgetController@show', 'as' => 'show']); Route::get('show/{budget}/{budgetlimit}', ['uses' => 'BudgetController@showByBudgetLimit', 'as' => 'show.limit']); Route::get('list/no-budget/{moment?}', ['uses' => 'BudgetController@noBudget', 'as' => 'no-budget']); + Route::get('{moment?}', ['uses' => 'BudgetController@index', 'as' => 'index']); Route::post('income', ['uses' => 'BudgetController@postUpdateIncome', 'as' => 'income.post']); Route::post('store', ['uses' => 'BudgetController@store', 'as' => 'store']); diff --git a/tests/Feature/Controllers/BudgetControllerTest.php b/tests/Feature/Controllers/BudgetControllerTest.php index b79a537f54..f740ec293f 100644 --- a/tests/Feature/Controllers/BudgetControllerTest.php +++ b/tests/Feature/Controllers/BudgetControllerTest.php @@ -444,14 +444,16 @@ class BudgetControllerTest extends TestCase */ public function testUpdateIncome() { + // must be in list + $this->be($this->user()); + // mock stuff $repository = $this->mock(BudgetRepositoryInterface::class); $journalRepos = $this->mock(JournalRepositoryInterface::class); $journalRepos->shouldReceive('first')->once()->andReturn(new TransactionJournal); $repository->shouldReceive('getAvailableBudget')->andReturn('1'); + $repository->shouldReceive('cleanupBudgets'); - // must be in list - $this->be($this->user()); $response = $this->get(route('budgets.income', [1])); $response->assertStatus(200); } diff --git a/tests/Feature/Controllers/Transaction/MassControllerTest.php b/tests/Feature/Controllers/Transaction/MassControllerTest.php index 065d66a9e3..988dfaf3ac 100644 --- a/tests/Feature/Controllers/Transaction/MassControllerTest.php +++ b/tests/Feature/Controllers/Transaction/MassControllerTest.php @@ -7,7 +7,7 @@ * See the LICENSE file for details. */ -declare(strict_types = 1); +declare(strict_types=1); namespace Tests\Feature\Controllers\Transaction; @@ -18,7 +18,6 @@ use FireflyIII\Models\TransactionJournal; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; -use FireflyIII\Repositories\Journal\JournalUpdateInterface; use Illuminate\Support\Collection; use Tests\TestCase; @@ -172,9 +171,8 @@ class MassControllerTest extends TestCase ->first(); // mock stuff $repository = $this->mock(JournalRepositoryInterface::class); - $updater = $this->mock(JournalUpdateInterface::class); $repository->shouldReceive('first')->once()->andReturn(new TransactionJournal); - $updater->shouldReceive('update')->once(); + $repository->shouldReceive('update')->once(); $repository->shouldReceive('find')->once()->andReturn($deposit); diff --git a/tests/Feature/Controllers/Transaction/SingleControllerTest.php b/tests/Feature/Controllers/Transaction/SingleControllerTest.php index b396a961db..56bd1041bf 100644 --- a/tests/Feature/Controllers/Transaction/SingleControllerTest.php +++ b/tests/Feature/Controllers/Transaction/SingleControllerTest.php @@ -23,7 +23,6 @@ use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; -use FireflyIII\Repositories\Journal\JournalUpdateInterface; use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface; use Illuminate\Support\Collection; use Illuminate\Support\MessageBag; @@ -270,7 +269,6 @@ class SingleControllerTest extends TestCase // mock $this->expectsEvents(UpdatedTransactionJournal::class); - $updater = $this->mock(JournalUpdateInterface::class); $repository = $this->mock(JournalRepositoryInterface::class); $journal = new TransactionJournal(); @@ -280,7 +278,7 @@ class SingleControllerTest extends TestCase $journal->transactionType()->associate($type); - $updater->shouldReceive('update')->andReturn($journal); + $repository->shouldReceive('update')->andReturn($journal); $repository->shouldReceive('first')->times(2)->andReturn(new TransactionJournal); $this->session(['transactions.edit.uri' => 'http://localhost']); diff --git a/tests/Feature/Controllers/Transaction/SplitControllerTest.php b/tests/Feature/Controllers/Transaction/SplitControllerTest.php index c6cf00eb07..578802bc8c 100644 --- a/tests/Feature/Controllers/Transaction/SplitControllerTest.php +++ b/tests/Feature/Controllers/Transaction/SplitControllerTest.php @@ -21,7 +21,6 @@ use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use FireflyIII\Repositories\Journal\JournalTaskerInterface; -use FireflyIII\Repositories\Journal\JournalUpdateInterface; use Illuminate\Support\Collection; use Illuminate\Support\MessageBag; use Tests\TestCase; @@ -135,8 +134,7 @@ class SplitControllerTest extends TestCase // mock stuff $repository = $this->mock(JournalRepositoryInterface::class); - $updater = $this->mock(JournalUpdateInterface::class); - $updater->shouldReceive('updateSplitJournal')->andReturn($deposit); + $repository->shouldReceive('updateSplitJournal')->andReturn($deposit); $repository->shouldReceive('first')->times(2)->andReturn(new TransactionJournal); $attachmentRepos = $this->mock(AttachmentHelperInterface::class); $attachmentRepos->shouldReceive('saveAttachmentsForModel'); diff --git a/tests/Unit/Handlers/Events/UserEventHandlerTest.php b/tests/Unit/Handlers/Events/UserEventHandlerTest.php index 0edb4c3938..27b44dfb72 100644 --- a/tests/Unit/Handlers/Events/UserEventHandlerTest.php +++ b/tests/Unit/Handlers/Events/UserEventHandlerTest.php @@ -56,7 +56,7 @@ class UserEventHandlerTest extends TestCase Mail::assertSent( RequestedNewPasswordMail::class, function ($mail) use ($user) { - return $mail->hasTo($user->email) && $mail->ip === '127.0.0.1'; + return $mail->hasTo($user->email) && $mail->ipAddress === '127.0.0.1'; } ); @@ -78,7 +78,7 @@ class UserEventHandlerTest extends TestCase // must send user an email: Mail::assertSent( RegisteredUserMail::class, function ($mail) use ($user) { - return $mail->hasTo($user->email) && $mail->ip === '127.0.0.1'; + return $mail->hasTo($user->email) && $mail->ipAddress === '127.0.0.1'; } ); From 935fb015d3aec2860634d907cbda026972cba013 Mon Sep 17 00:00:00 2001 From: James Cole Date: Wed, 7 Jun 2017 11:58:04 +0200 Subject: [PATCH 037/126] Live update budget amounts. --- app/Http/Controllers/BudgetController.php | 3 +- app/Support/Amount.php | 2 +- public/js/ff/budgets/index.js | 17 ++++++++ resources/views/budgets/index.twig | 53 +---------------------- 4 files changed, 22 insertions(+), 53 deletions(-) diff --git a/app/Http/Controllers/BudgetController.php b/app/Http/Controllers/BudgetController.php index 1e4545d518..329f46fdc8 100644 --- a/app/Http/Controllers/BudgetController.php +++ b/app/Http/Controllers/BudgetController.php @@ -241,7 +241,8 @@ class BudgetController extends Controller return view( 'budgets.index', compact( - 'available', 'currentMonth', 'next', 'nextText', 'prev', 'prevText', 'periodStart', 'periodEnd', 'budgetInformation', 'inactive', 'budgets', + 'available', 'currentMonth', 'next', 'nextText', 'prev', 'prevText', + 'periodStart', 'periodEnd', 'budgetInformation', 'inactive', 'budgets', 'spent', 'budgeted', 'previousLoop', 'nextLoop', 'start' ) ); diff --git a/app/Support/Amount.php b/app/Support/Amount.php index d90a1a2de4..89ef0dea05 100644 --- a/app/Support/Amount.php +++ b/app/Support/Amount.php @@ -204,7 +204,7 @@ class Amount } /** - * @return TransactionCurrency + * @return \FireflyIII\Models\TransactionCurrency * @throws FireflyException */ public function getDefaultCurrency(): TransactionCurrency diff --git a/public/js/ff/budgets/index.js b/public/js/ff/budgets/index.js index ac233b85c4..01a00c9b0f 100644 --- a/public/js/ff/budgets/index.js +++ b/public/js/ff/budgets/index.js @@ -59,9 +59,26 @@ function updateBudgetedAmounts(e) { "use strict"; var target = $(e.target); var id = target.data('id'); + var value = target.val(); var original = target.data('original'); var difference = value - original; + + var spentCell = $('td[class="spent"][data-id="' + id + '"]'); + var leftCell = $('td[class="left"][data-id="' + id + '"]'); + var spentAmount = parseFloat(spentCell.data('spent')); + var newAmountLeft = spentAmount + parseFloat(value); + var amountLeftString = accounting.formatMoney(newAmountLeft); + if(newAmountLeft < 0) { + leftCell.html('' + amountLeftString + ''); + } + if(newAmountLeft > 0) { + leftCell.html('' + amountLeftString + ''); + } + if(newAmountLeft === 0.0) { + leftCell.html('' + amountLeftString + ''); + } + if (difference !== 0) { // add difference to 'budgeted' var budgeted = budgeted + difference; diff --git a/resources/views/budgets/index.twig b/resources/views/budgets/index.twig index 240b7e9845..00214b8c6c 100644 --- a/resources/views/budgets/index.twig +++ b/resources/views/budgets/index.twig @@ -176,61 +176,12 @@ step="1" min="0" name="amount" type="number">
- + {{ budgetInformation[budget.id]['spent']|formatAmount }} - + {{ (repAmount + budgetInformation[budget.id]['spent'])|formatAmount }} - {# -
- - - - - - - - - - {% if budgetInformation[budget.id]['otherLimits'].count > 0 %} - - - - {% endif %} -
- - - -
- -
-
- {{ 'spent'|_ }} -
- {{ session('start').formatLocalized(monthAndDayFormat) }} - - {{ session('end').formatLocalized(monthAndDayFormat) }} -
-
- -
-
    - {% for other in budgetInformation[budget.id]['otherLimits'] %} -
  • - - Budgeted - {{ other.amount|formatAmountPlain }} - between - {{ other.start_date.formatLocalized(monthAndDayFormat) }} - and {{ other.end_date.formatLocalized(monthAndDayFormat) }}. -
  • - {% endfor %} -
-
-
-
- - #} {% endfor %} From d8a8574ddace25ad03a126b20b53ef9e9ca13bf5 Mon Sep 17 00:00:00 2001 From: James Cole Date: Wed, 7 Jun 2017 12:08:32 +0200 Subject: [PATCH 038/126] Prep for new release. --- CHANGELOG.md | 13 +++++++++++++ config/firefly.php | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a64c684432..33a9707ff9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,19 @@ All notable changes to this project will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org/). +## [4.5.0] - 2017-07-07 + +### Added +- Better support for multi-currency transactions and display of transactions, accounts and everything. This requires a database overhaul (moving the currency information to specific transactions) so be careful when upgrading. +- Translations for Spanish and Slovenian. +- New interface for budget page, ~~stolen from~~ inspired by YNAB. +- Expanded Docker to work with postgresql as well, thanks to @kressh + +### Fixed +- PostgreSQL support in database upgrade routine (#644, reported by @) +- Frontpage budget chart was off, fix by @nhaarman +- Was not possible to remove opening balance. + ## [4.4.3] - 2017-05-03 ### Added - Added support for Slovenian diff --git a/config/firefly.php b/config/firefly.php index cb76de0723..c4f648e3a4 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -23,7 +23,7 @@ return [ 'is_demo_site' => false, ], 'encryption' => (is_null(env('USE_ENCRYPTION')) || env('USE_ENCRYPTION') === true), - 'version' => '4.4.3', + 'version' => '4.5.0', 'maxUploadSize' => 5242880, 'allowedMimes' => ['image/png', 'image/jpeg', 'application/pdf'], 'list_length' => 10, From 5c1879412212d9864b3a70bc0d720d1f94b1a309 Mon Sep 17 00:00:00 2001 From: James Cole Date: Wed, 7 Jun 2017 12:22:59 +0200 Subject: [PATCH 039/126] Update lock file, and update database. --- composer.lock | 294 +++++++++++++++------------ storage/database/databasecopy.sqlite | Bin 258048 -> 272384 bytes 2 files changed, 159 insertions(+), 135 deletions(-) diff --git a/composer.lock b/composer.lock index 5363e7571a..cef2489c1d 100644 --- a/composer.lock +++ b/composer.lock @@ -665,16 +665,16 @@ }, { "name": "laravel/framework", - "version": "v5.4.21", + "version": "v5.4.24", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "2ed668f96d1a6ca42f50d5c87ee9ceecfc0a6eee" + "reference": "ec8548db26c1b147570f661128649e98f3ac0f29" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/2ed668f96d1a6ca42f50d5c87ee9ceecfc0a6eee", - "reference": "2ed668f96d1a6ca42f50d5c87ee9ceecfc0a6eee", + "url": "https://api.github.com/repos/laravel/framework/zipball/ec8548db26c1b147570f661128649e98f3ac0f29", + "reference": "ec8548db26c1b147570f661128649e98f3ac0f29", "shasum": "" }, "require": { @@ -790,20 +790,20 @@ "framework", "laravel" ], - "time": "2017-04-28T15:40:01+00:00" + "time": "2017-05-30T12:44:32+00:00" }, { "name": "laravelcollective/html", - "version": "v5.4.1", + "version": "v5.4.8", "source": { "type": "git", "url": "https://github.com/LaravelCollective/html.git", - "reference": "7570f25d58a00fd6909c0563808590f9cdb14d47" + "reference": "9b8f51e7a2368911c896f5d42757886bae0717b5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/LaravelCollective/html/zipball/7570f25d58a00fd6909c0563808590f9cdb14d47", - "reference": "7570f25d58a00fd6909c0563808590f9cdb14d47", + "url": "https://api.github.com/repos/LaravelCollective/html/zipball/9b8f51e7a2368911c896f5d42757886bae0717b5", + "reference": "9b8f51e7a2368911c896f5d42757886bae0717b5", "shasum": "" }, "require": { @@ -844,20 +844,20 @@ ], "description": "HTML and Form Builders for the Laravel Framework", "homepage": "http://laravelcollective.com", - "time": "2017-01-26T19:27:05+00:00" + "time": "2017-05-22T06:35:07+00:00" }, { "name": "league/commonmark", - "version": "0.15.3", + "version": "0.15.4", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "c8b43ee5821362216f8e9ac684f0f59de164edcc" + "reference": "c4c8e6bf99e62d9568875d9fc3ef473fe3e18e0c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/c8b43ee5821362216f8e9ac684f0f59de164edcc", - "reference": "c8b43ee5821362216f8e9ac684f0f59de164edcc", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/c4c8e6bf99e62d9568875d9fc3ef473fe3e18e0c", + "reference": "c4c8e6bf99e62d9568875d9fc3ef473fe3e18e0c", "shasum": "" }, "require": { @@ -913,7 +913,7 @@ "markdown", "parser" ], - "time": "2016-12-19T00:11:43+00:00" + "time": "2017-05-09T12:47:53+00:00" }, { "name": "league/csv", @@ -1583,16 +1583,16 @@ }, { "name": "swiftmailer/swiftmailer", - "version": "v5.4.7", + "version": "v5.4.8", "source": { "type": "git", "url": "https://github.com/swiftmailer/swiftmailer.git", - "reference": "56db4ed32a6d5c9824c3ecc1d2e538f663f47eb4" + "reference": "9a06dc570a0367850280eefd3f1dc2da45aef517" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/56db4ed32a6d5c9824c3ecc1d2e538f663f47eb4", - "reference": "56db4ed32a6d5c9824c3ecc1d2e538f663f47eb4", + "url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/9a06dc570a0367850280eefd3f1dc2da45aef517", + "reference": "9a06dc570a0367850280eefd3f1dc2da45aef517", "shasum": "" }, "require": { @@ -1633,20 +1633,20 @@ "mail", "mailer" ], - "time": "2017-04-20T17:32:18+00:00" + "time": "2017-05-01T15:54:03+00:00" }, { "name": "symfony/console", - "version": "v3.2.8", + "version": "v3.3.2", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "a7a17e0c6c3c4d70a211f80782e4b90ddadeaa38" + "reference": "70d2a29b2911cbdc91a7e268046c395278238b2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/a7a17e0c6c3c4d70a211f80782e4b90ddadeaa38", - "reference": "a7a17e0c6c3c4d70a211f80782e4b90ddadeaa38", + "url": "https://api.github.com/repos/symfony/console/zipball/70d2a29b2911cbdc91a7e268046c395278238b2e", + "reference": "70d2a29b2911cbdc91a7e268046c395278238b2e", "shasum": "" }, "require": { @@ -1654,10 +1654,16 @@ "symfony/debug": "~2.8|~3.0", "symfony/polyfill-mbstring": "~1.0" }, + "conflict": { + "symfony/dependency-injection": "<3.3" + }, "require-dev": { "psr/log": "~1.0", + "symfony/config": "~3.3", + "symfony/dependency-injection": "~3.3", "symfony/event-dispatcher": "~2.8|~3.0", "symfony/filesystem": "~2.8|~3.0", + "symfony/http-kernel": "~2.8|~3.0", "symfony/process": "~2.8|~3.0" }, "suggest": { @@ -1669,7 +1675,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -1696,7 +1702,7 @@ ], "description": "Symfony Console Component", "homepage": "https://symfony.com", - "time": "2017-04-26T01:39:17+00:00" + "time": "2017-06-02T19:24:58+00:00" }, { "name": "symfony/css-selector", @@ -1753,16 +1759,16 @@ }, { "name": "symfony/debug", - "version": "v3.2.8", + "version": "v3.3.2", "source": { "type": "git", "url": "https://github.com/symfony/debug.git", - "reference": "fd6eeee656a5a7b384d56f1072243fe1c0e81686" + "reference": "e9c50482841ef696e8fa1470d950a79c8921f45d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/debug/zipball/fd6eeee656a5a7b384d56f1072243fe1c0e81686", - "reference": "fd6eeee656a5a7b384d56f1072243fe1c0e81686", + "url": "https://api.github.com/repos/symfony/debug/zipball/e9c50482841ef696e8fa1470d950a79c8921f45d", + "reference": "e9c50482841ef696e8fa1470d950a79c8921f45d", "shasum": "" }, "require": { @@ -1773,13 +1779,12 @@ "symfony/http-kernel": ">=2.3,<2.3.24|~2.4.0|>=2.5,<2.5.9|>=2.6,<2.6.2" }, "require-dev": { - "symfony/class-loader": "~2.8|~3.0", "symfony/http-kernel": "~2.8|~3.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -1806,11 +1811,11 @@ ], "description": "Symfony Debug Component", "homepage": "https://symfony.com", - "time": "2017-04-19T20:17:50+00:00" + "time": "2017-06-01T21:01:25+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v2.8.20", + "version": "v2.8.21", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", @@ -1870,16 +1875,16 @@ }, { "name": "symfony/finder", - "version": "v3.2.8", + "version": "v3.3.2", "source": { "type": "git", "url": "https://github.com/symfony/finder.git", - "reference": "9cf076f8f492f4b1ffac40aae9c2d287b4ca6930" + "reference": "baea7f66d30854ad32988c11a09d7ffd485810c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/finder/zipball/9cf076f8f492f4b1ffac40aae9c2d287b4ca6930", - "reference": "9cf076f8f492f4b1ffac40aae9c2d287b4ca6930", + "url": "https://api.github.com/repos/symfony/finder/zipball/baea7f66d30854ad32988c11a09d7ffd485810c4", + "reference": "baea7f66d30854ad32988c11a09d7ffd485810c4", "shasum": "" }, "require": { @@ -1888,7 +1893,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -1915,20 +1920,20 @@ ], "description": "Symfony Finder Component", "homepage": "https://symfony.com", - "time": "2017-04-12T14:13:17+00:00" + "time": "2017-06-01T21:01:25+00:00" }, { "name": "symfony/http-foundation", - "version": "v3.2.8", + "version": "v3.3.2", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "9de6add7f731e5af7f5b2e9c0da365e43383ebef" + "reference": "80eb5a1f968448b77da9e8b2c0827f6e8d767846" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/9de6add7f731e5af7f5b2e9c0da365e43383ebef", - "reference": "9de6add7f731e5af7f5b2e9c0da365e43383ebef", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/80eb5a1f968448b77da9e8b2c0827f6e8d767846", + "reference": "80eb5a1f968448b77da9e8b2c0827f6e8d767846", "shasum": "" }, "require": { @@ -1941,7 +1946,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -1968,20 +1973,20 @@ ], "description": "Symfony HttpFoundation Component", "homepage": "https://symfony.com", - "time": "2017-05-01T14:55:58+00:00" + "time": "2017-06-05T13:06:51+00:00" }, { "name": "symfony/http-kernel", - "version": "v3.2.8", + "version": "v3.3.0", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "46e8b209abab55c072c47d72d5cd1d62c0585e05" + "reference": "4ad34a0d20a5848c0fcbf6ff6a2ff1cd9cf4b9ed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/46e8b209abab55c072c47d72d5cd1d62c0585e05", - "reference": "46e8b209abab55c072c47d72d5cd1d62c0585e05", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/4ad34a0d20a5848c0fcbf6ff6a2ff1cd9cf4b9ed", + "reference": "4ad34a0d20a5848c0fcbf6ff6a2ff1cd9cf4b9ed", "shasum": "" }, "require": { @@ -1989,18 +1994,21 @@ "psr/log": "~1.0", "symfony/debug": "~2.8|~3.0", "symfony/event-dispatcher": "~2.8|~3.0", - "symfony/http-foundation": "~2.8.13|~3.1.6|~3.2" + "symfony/http-foundation": "~3.3" }, "conflict": { - "symfony/config": "<2.8" + "symfony/config": "<2.8", + "symfony/dependency-injection": "<3.3", + "symfony/var-dumper": "<3.3" }, "require-dev": { + "psr/cache": "~1.0", "symfony/browser-kit": "~2.8|~3.0", "symfony/class-loader": "~2.8|~3.0", "symfony/config": "~2.8|~3.0", "symfony/console": "~2.8|~3.0", "symfony/css-selector": "~2.8|~3.0", - "symfony/dependency-injection": "~2.8|~3.0", + "symfony/dependency-injection": "~3.3", "symfony/dom-crawler": "~2.8|~3.0", "symfony/expression-language": "~2.8|~3.0", "symfony/finder": "~2.8|~3.0", @@ -2009,7 +2017,7 @@ "symfony/stopwatch": "~2.8|~3.0", "symfony/templating": "~2.8|~3.0", "symfony/translation": "~2.8|~3.0", - "symfony/var-dumper": "~3.2" + "symfony/var-dumper": "~3.3" }, "suggest": { "symfony/browser-kit": "", @@ -2023,7 +2031,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -2050,7 +2058,7 @@ ], "description": "Symfony HttpKernel Component", "homepage": "https://symfony.com", - "time": "2017-05-01T17:46:48+00:00" + "time": "2017-05-29T21:02:12+00:00" }, { "name": "symfony/polyfill-mbstring", @@ -2221,16 +2229,16 @@ }, { "name": "symfony/process", - "version": "v3.2.8", + "version": "v3.3.2", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "999c2cf5061e627e6cd551dc9ebf90dd1d11d9f0" + "reference": "8e30690c67aafb6c7992d6d8eb0d707807dd3eaf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/999c2cf5061e627e6cd551dc9ebf90dd1d11d9f0", - "reference": "999c2cf5061e627e6cd551dc9ebf90dd1d11d9f0", + "url": "https://api.github.com/repos/symfony/process/zipball/8e30690c67aafb6c7992d6d8eb0d707807dd3eaf", + "reference": "8e30690c67aafb6c7992d6d8eb0d707807dd3eaf", "shasum": "" }, "require": { @@ -2239,7 +2247,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -2266,36 +2274,39 @@ ], "description": "Symfony Process Component", "homepage": "https://symfony.com", - "time": "2017-04-12T14:13:17+00:00" + "time": "2017-05-22T12:32:03+00:00" }, { "name": "symfony/routing", - "version": "v3.2.8", + "version": "v3.3.2", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "5029745d6d463585e8b487dbc83d6333f408853a" + "reference": "39804eeafea5cca851946e1eed122eb94459fdb4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/5029745d6d463585e8b487dbc83d6333f408853a", - "reference": "5029745d6d463585e8b487dbc83d6333f408853a", + "url": "https://api.github.com/repos/symfony/routing/zipball/39804eeafea5cca851946e1eed122eb94459fdb4", + "reference": "39804eeafea5cca851946e1eed122eb94459fdb4", "shasum": "" }, "require": { "php": ">=5.5.9" }, "conflict": { - "symfony/config": "<2.8" + "symfony/config": "<2.8", + "symfony/dependency-injection": "<3.3", + "symfony/yaml": "<3.3" }, "require-dev": { "doctrine/annotations": "~1.0", "doctrine/common": "~2.2", "psr/log": "~1.0", "symfony/config": "~2.8|~3.0", + "symfony/dependency-injection": "~3.3", "symfony/expression-language": "~2.8|~3.0", "symfony/http-foundation": "~2.8|~3.0", - "symfony/yaml": "~2.8|~3.0" + "symfony/yaml": "~3.3" }, "suggest": { "doctrine/annotations": "For using the annotation loader", @@ -2308,7 +2319,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -2341,20 +2352,20 @@ "uri", "url" ], - "time": "2017-04-12T14:13:17+00:00" + "time": "2017-06-02T09:51:43+00:00" }, { "name": "symfony/translation", - "version": "v3.2.8", + "version": "v3.3.2", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "f4a04d2df710f81515df576b2de06bdeee518b83" + "reference": "dc3b2a0c6cfff60327ba1c043a82092735397543" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/f4a04d2df710f81515df576b2de06bdeee518b83", - "reference": "f4a04d2df710f81515df576b2de06bdeee518b83", + "url": "https://api.github.com/repos/symfony/translation/zipball/dc3b2a0c6cfff60327ba1c043a82092735397543", + "reference": "dc3b2a0c6cfff60327ba1c043a82092735397543", "shasum": "" }, "require": { @@ -2362,13 +2373,14 @@ "symfony/polyfill-mbstring": "~1.0" }, "conflict": { - "symfony/config": "<2.8" + "symfony/config": "<2.8", + "symfony/yaml": "<3.3" }, "require-dev": { "psr/log": "~1.0", "symfony/config": "~2.8|~3.0", "symfony/intl": "^2.8.18|^3.2.5", - "symfony/yaml": "~2.8|~3.0" + "symfony/yaml": "~3.3" }, "suggest": { "psr/log": "To use logging capability in translator", @@ -2378,7 +2390,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -2405,20 +2417,20 @@ ], "description": "Symfony Translation Component", "homepage": "https://symfony.com", - "time": "2017-04-12T14:13:17+00:00" + "time": "2017-05-22T07:42:36+00:00" }, { "name": "symfony/var-dumper", - "version": "v3.2.8", + "version": "v3.3.2", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "fa47963ac7979ddbd42b2d646d1b056bddbf7bb8" + "reference": "347c4247a3e40018810b476fcd5dec36d46d08dc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/fa47963ac7979ddbd42b2d646d1b056bddbf7bb8", - "reference": "fa47963ac7979ddbd42b2d646d1b056bddbf7bb8", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/347c4247a3e40018810b476fcd5dec36d46d08dc", + "reference": "347c4247a3e40018810b476fcd5dec36d46d08dc", "shasum": "" }, "require": { @@ -2430,7 +2442,7 @@ }, "require-dev": { "ext-iconv": "*", - "twig/twig": "~1.20|~2.0" + "twig/twig": "~1.34|~2.4" }, "suggest": { "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", @@ -2439,7 +2451,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -2473,7 +2485,7 @@ "debug", "dump" ], - "time": "2017-05-01T14:55:58+00:00" + "time": "2017-06-02T09:10:29+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -2687,20 +2699,20 @@ "packages-dev": [ { "name": "barryvdh/laravel-debugbar", - "version": "v2.3.2", + "version": "v2.4.0", "source": { "type": "git", "url": "https://github.com/barryvdh/laravel-debugbar.git", - "reference": "24e4f0261e352d3fd86d0447791b56ae49398674" + "reference": "de15d00a74696db62e1b4782474c27ed0c4fc763" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/24e4f0261e352d3fd86d0447791b56ae49398674", - "reference": "24e4f0261e352d3fd86d0447791b56ae49398674", + "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/de15d00a74696db62e1b4782474c27ed0c4fc763", + "reference": "de15d00a74696db62e1b4782474c27ed0c4fc763", "shasum": "" }, "require": { - "illuminate/support": "5.1.*|5.2.*|5.3.*|5.4.*", + "illuminate/support": "5.1.*|5.2.*|5.3.*|5.4.*|5.5.*", "maximebf/debugbar": "~1.13.0", "php": ">=5.5.9", "symfony/finder": "~2.7|~3.0" @@ -2708,7 +2720,15 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.4-dev" + }, + "laravel": { + "providers": [ + "Barryvdh\\Debugbar\\ServiceProvider" + ], + "aliases": { + "Debugbar": "Barryvdh\\Debugbar\\Facade" + } } }, "autoload": { @@ -2737,7 +2757,7 @@ "profiler", "webprofiler" ], - "time": "2017-01-19T08:19:49+00:00" + "time": "2017-06-01T17:46:08+00:00" }, { "name": "barryvdh/laravel-ide-helper", @@ -3725,16 +3745,16 @@ }, { "name": "phpunit/phpunit", - "version": "5.7.19", + "version": "5.7.20", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "69c4f49ff376af2692bad9cebd883d17ebaa98a1" + "reference": "3cb94a5f8c07a03c8b7527ed7468a2926203f58b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/69c4f49ff376af2692bad9cebd883d17ebaa98a1", - "reference": "69c4f49ff376af2692bad9cebd883d17ebaa98a1", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3cb94a5f8c07a03c8b7527ed7468a2926203f58b", + "reference": "3cb94a5f8c07a03c8b7527ed7468a2926203f58b", "shasum": "" }, "require": { @@ -3752,7 +3772,7 @@ "phpunit/php-timer": "^1.0.6", "phpunit/phpunit-mock-objects": "^3.2", "sebastian/comparator": "^1.2.4", - "sebastian/diff": "~1.2", + "sebastian/diff": "^1.4.3", "sebastian/environment": "^1.3.4 || ^2.0", "sebastian/exporter": "~2.0", "sebastian/global-state": "^1.1", @@ -3803,7 +3823,7 @@ "testing", "xunit" ], - "time": "2017-04-03T02:22:27+00:00" + "time": "2017-05-22T07:42:55+00:00" }, { "name": "phpunit/phpunit-mock-objects", @@ -4033,23 +4053,23 @@ }, { "name": "sebastian/diff", - "version": "1.4.1", + "version": "1.4.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e" + "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e", - "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7f066a26a962dbe58ddea9f72a4e82874a3975a4", + "reference": "7f066a26a962dbe58ddea9f72a4e82874a3975a4", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": "^5.3.3 || ^7.0" }, "require-dev": { - "phpunit/phpunit": "~4.8" + "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" }, "type": "library", "extra": { @@ -4081,7 +4101,7 @@ "keywords": [ "diff" ], - "time": "2015-12-08T07:14:41+00:00" + "time": "2017-05-22T07:24:03+00:00" }, { "name": "sebastian/environment", @@ -4437,16 +4457,16 @@ }, { "name": "symfony/class-loader", - "version": "v3.2.8", + "version": "v3.3.2", "source": { "type": "git", "url": "https://github.com/symfony/class-loader.git", - "reference": "fc4c04bfd17130a9dccfded9578353f311967da7" + "reference": "386a294d621576302e7cc36965d6ed53b8c73c4f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/class-loader/zipball/fc4c04bfd17130a9dccfded9578353f311967da7", - "reference": "fc4c04bfd17130a9dccfded9578353f311967da7", + "url": "https://api.github.com/repos/symfony/class-loader/zipball/386a294d621576302e7cc36965d6ed53b8c73c4f", + "reference": "386a294d621576302e7cc36965d6ed53b8c73c4f", "shasum": "" }, "require": { @@ -4462,7 +4482,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -4489,27 +4509,31 @@ ], "description": "Symfony ClassLoader Component", "homepage": "https://symfony.com", - "time": "2017-04-12T14:13:17+00:00" + "time": "2017-06-02T09:51:43+00:00" }, { "name": "symfony/config", - "version": "v3.2.8", + "version": "v3.3.2", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "e5533fcc0b3dd377626153b2852707878f363728" + "reference": "35716d4904e0506a7a5a9bcf23f854aeb5719bca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/e5533fcc0b3dd377626153b2852707878f363728", - "reference": "e5533fcc0b3dd377626153b2852707878f363728", + "url": "https://api.github.com/repos/symfony/config/zipball/35716d4904e0506a7a5a9bcf23f854aeb5719bca", + "reference": "35716d4904e0506a7a5a9bcf23f854aeb5719bca", "shasum": "" }, "require": { "php": ">=5.5.9", "symfony/filesystem": "~2.8|~3.0" }, + "conflict": { + "symfony/dependency-injection": "<3.3" + }, "require-dev": { + "symfony/dependency-injection": "~3.3", "symfony/yaml": "~3.0" }, "suggest": { @@ -4518,7 +4542,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -4545,7 +4569,7 @@ ], "description": "Symfony Config Component", "homepage": "https://symfony.com", - "time": "2017-04-12T14:13:17+00:00" + "time": "2017-06-02T18:07:20+00:00" }, { "name": "symfony/dom-crawler", @@ -4605,16 +4629,16 @@ }, { "name": "symfony/filesystem", - "version": "v3.2.8", + "version": "v3.3.2", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "040651db13cf061827a460cc10f6e36a445c45b4" + "reference": "c709670bf64721202ddbe4162846f250735842c0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/040651db13cf061827a460cc10f6e36a445c45b4", - "reference": "040651db13cf061827a460cc10f6e36a445c45b4", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/c709670bf64721202ddbe4162846f250735842c0", + "reference": "c709670bf64721202ddbe4162846f250735842c0", "shasum": "" }, "require": { @@ -4623,7 +4647,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -4650,20 +4674,20 @@ ], "description": "Symfony Filesystem Component", "homepage": "https://symfony.com", - "time": "2017-04-12T14:13:17+00:00" + "time": "2017-05-28T14:08:56+00:00" }, { "name": "symfony/stopwatch", - "version": "v3.2.8", + "version": "v3.3.2", "source": { "type": "git", "url": "https://github.com/symfony/stopwatch.git", - "reference": "5a0105afb670dbd38f521105c444de1b8e10cfe3" + "reference": "602a15299dc01556013b07167d4f5d3a60e90d15" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/stopwatch/zipball/5a0105afb670dbd38f521105c444de1b8e10cfe3", - "reference": "5a0105afb670dbd38f521105c444de1b8e10cfe3", + "url": "https://api.github.com/repos/symfony/stopwatch/zipball/602a15299dc01556013b07167d4f5d3a60e90d15", + "reference": "602a15299dc01556013b07167d4f5d3a60e90d15", "shasum": "" }, "require": { @@ -4672,7 +4696,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -4699,20 +4723,20 @@ ], "description": "Symfony Stopwatch Component", "homepage": "https://symfony.com", - "time": "2017-04-12T14:13:17+00:00" + "time": "2017-04-12T14:14:56+00:00" }, { "name": "symfony/yaml", - "version": "v3.2.8", + "version": "v3.3.2", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "acec26fcf7f3031e094e910b94b002fa53d4e4d6" + "reference": "9752a30000a8ca9f4b34b5227d15d0101b96b063" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/acec26fcf7f3031e094e910b94b002fa53d4e4d6", - "reference": "acec26fcf7f3031e094e910b94b002fa53d4e4d6", + "url": "https://api.github.com/repos/symfony/yaml/zipball/9752a30000a8ca9f4b34b5227d15d0101b96b063", + "reference": "9752a30000a8ca9f4b34b5227d15d0101b96b063", "shasum": "" }, "require": { @@ -4727,7 +4751,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-master": "3.3-dev" } }, "autoload": { @@ -4754,7 +4778,7 @@ ], "description": "Symfony Yaml Component", "homepage": "https://symfony.com", - "time": "2017-05-01T14:55:58+00:00" + "time": "2017-06-02T22:05:06+00:00" }, { "name": "webmozart/assert", diff --git a/storage/database/databasecopy.sqlite b/storage/database/databasecopy.sqlite index bc987ad2207260701e9763971faaf72c896e62dd..03c6232934ff356d8404dcb955504ae5ae5f5e9a 100755 GIT binary patch literal 272384 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 literal 258048 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 From b48de98865cf288a508b8292c26d680d1a2d0696 Mon Sep 17 00:00:00 2001 From: James Cole Date: Thu, 8 Jun 2017 10:35:02 +0200 Subject: [PATCH 040/126] Fix a bug where the balance routine forgot to account for accounts without a currency preference. --- app/Http/Controllers/Chart/AccountController.php | 1 + app/Support/Steam.php | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/app/Http/Controllers/Chart/AccountController.php b/app/Http/Controllers/Chart/AccountController.php index c0af003e81..3771f913a8 100644 --- a/app/Http/Controllers/Chart/AccountController.php +++ b/app/Http/Controllers/Chart/AccountController.php @@ -127,6 +127,7 @@ class AccountController extends Controller $chartData[$account->name] = $diff; } } + arsort($chartData); $data = $this->generator->singleSet(strval(trans('firefly.spent')), $chartData); $cache->store($data); diff --git a/app/Support/Steam.php b/app/Support/Steam.php index 398400e05b..7294b07bde 100644 --- a/app/Support/Steam.php +++ b/app/Support/Steam.php @@ -13,6 +13,7 @@ declare(strict_types=1); namespace FireflyIII\Support; +use Amount; use Carbon\Carbon; use Crypt; use DB; @@ -47,6 +48,11 @@ class Steam return $cache->get(); // @codeCoverageIgnore } $currencyId = intval($account->getMeta('currency_id')); + // if null, use system default currency: + if ($currencyId === 0) { + $currency = Amount::getDefaultCurrency(); + $currencyId = $currency->id; + } // first part: get all balances in own currency: $nativeBalance = strval( $account->transactions() From a2145f6b490e77422d02aed0600fdb6c1732d212 Mon Sep 17 00:00:00 2001 From: James Cole Date: Thu, 8 Jun 2017 10:54:15 +0200 Subject: [PATCH 041/126] Possible fix for #667 --- app/Console/Commands/UpgradeDatabase.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/Console/Commands/UpgradeDatabase.php b/app/Console/Commands/UpgradeDatabase.php index 107f61f604..42566a6cee 100644 --- a/app/Console/Commands/UpgradeDatabase.php +++ b/app/Console/Commands/UpgradeDatabase.php @@ -316,6 +316,7 @@ class UpgradeDatabase extends Command $notification = '%s #%d uses %s but should use %s. It has been updated. Please verify this in Firefly III.'; $transfer = 'Transfer #%d has been updated to use the correct currencies. Please verify this in Firefly III.'; $driver = DB::connection()->getDriverName(); + $pgsql = ['pgsql', 'postgresql']; foreach ($types as $type => $operator) { $query = TransactionJournal @@ -328,10 +329,10 @@ class UpgradeDatabase extends Command ->leftJoin('account_meta', 'account_meta.account_id', '=', 'accounts.id') ->where('transaction_types.type', $type) ->where('account_meta.name', 'currency_id'); - if ($driver === 'postgresql') { + if (in_array($driver, $pgsql)) { $query->where('transaction_journals.transaction_currency_id', '!=', DB::raw('cast(account_meta.data as int)')); } - if ($driver !== 'postgresql') { + if (!in_array($driver, $pgsql)) { $query->where('transaction_journals.transaction_currency_id', '!=', DB::raw('account_meta.data')); } From 762d7bcc34a85ac4e65adf4fe5811fcc717dd268 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 9 Jun 2017 11:51:59 +0200 Subject: [PATCH 042/126] Fix database for postgresql --- database/migrations/2016_06_16_000002_create_main_tables.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/migrations/2016_06_16_000002_create_main_tables.php b/database/migrations/2016_06_16_000002_create_main_tables.php index 0f5fe8d4e4..300024918a 100644 --- a/database/migrations/2016_06_16_000002_create_main_tables.php +++ b/database/migrations/2016_06_16_000002_create_main_tables.php @@ -474,7 +474,7 @@ class CreateMainTables extends Migration $table->text('description')->nullable(); $table->decimal('latitude', 24, 12)->nullable(); $table->decimal('longitude', 24, 12)->nullable(); - $table->boolean('zoomLevel')->nullable(); + $table->smallInteger('zoomLevel', false, true)->nullable(); // link user id to users table $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); From 1f9b7faa6097c9a523b090099a57d192d1ed2af7 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 9 Jun 2017 11:52:20 +0200 Subject: [PATCH 043/126] Code for #660 --- app/Import/ImportStorage.php | 20 +++++++++++--------- app/Support/Import/CsvImportSupportTrait.php | 3 +++ 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/app/Import/ImportStorage.php b/app/Import/ImportStorage.php index 2d5524fcec..37808b6394 100644 --- a/app/Import/ImportStorage.php +++ b/app/Import/ImportStorage.php @@ -361,17 +361,19 @@ class ImportStorage // create new transactions. This is something that needs a rewrite for multiple/split transactions. $sourceData = [ - 'account_id' => $accounts['source']->id, - 'transaction_journal_id' => $journal->id, - 'description' => null, - 'amount' => bcmul($amount, '-1'), + 'account_id' => $accounts['source']->id, + 'transaction_journal_id' => $journal->id, + 'transaction_currency_id' => $journal->transaction_currency_id, + 'description' => null, + 'amount' => bcmul($amount, '-1'), ]; $destinationData = [ - 'account_id' => $accounts['destination']->id, - 'transaction_journal_id' => $journal->id, - 'description' => null, - 'amount' => $amount, + 'account_id' => $accounts['destination']->id, + 'transaction_currency_id' => $journal->transaction_currency_id, + 'transaction_journal_id' => $journal->id, + 'description' => null, + 'amount' => $amount, ]; $one = Transaction::create($sourceData); @@ -383,7 +385,7 @@ class ImportStorage } if (is_null($two->id)) { - Log::error('Could not create transaction 1.', $two->getErrors()->all()); + Log::error('Could not create transaction 2.', $two->getErrors()->all()); $error = true; } diff --git a/app/Support/Import/CsvImportSupportTrait.php b/app/Support/Import/CsvImportSupportTrait.php index efee16704a..dbf915f50f 100644 --- a/app/Support/Import/CsvImportSupportTrait.php +++ b/app/Support/Import/CsvImportSupportTrait.php @@ -16,12 +16,15 @@ use FireflyIII\Exceptions\FireflyException; use FireflyIII\Import\Mapper\MapperInterface; use FireflyIII\Import\MapperPreProcess\PreProcessorInterface; use FireflyIII\Import\Specifics\SpecificInterface; +use FireflyIII\Models\ImportJob; use League\Csv\Reader; use Log; /** * Trait CsvImportSupportTrait * + * @property ImportJob $job + * * @package FireflyIII\Support\Import */ trait CsvImportSupportTrait From 0b4efe4ae1042c4cdf68d6ac37572883d0b71871 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 9 Jun 2017 12:53:31 +0200 Subject: [PATCH 044/126] Small typo in chart. [skip ci] --- app/Http/Controllers/Chart/AccountController.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/Http/Controllers/Chart/AccountController.php b/app/Http/Controllers/Chart/AccountController.php index 3771f913a8..61194faff9 100644 --- a/app/Http/Controllers/Chart/AccountController.php +++ b/app/Http/Controllers/Chart/AccountController.php @@ -425,7 +425,7 @@ class AccountController extends Controller } arsort($chartData); - $data = $this->generator->singleSet(strval(trans('firefly.spent')), $chartData); + $data = $this->generator->singleSet(strval(trans('firefly.earned')), $chartData); $cache->store($data); return Response::json($data); From 091596e80eb45156ea3592921c93bba5fcb1b477 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 10 Jun 2017 15:09:41 +0200 Subject: [PATCH 045/126] Lots of new code for new importer routine. --- app/Http/Controllers/ImportController.php | 561 ++++++++---------- app/Http/Requests/ImportUploadRequest.php | 5 +- .../Configurator/ConfiguratorInterface.php | 60 ++ app/Import/Configurator/CsvConfigurator.php | 140 +++++ .../{CsvImporter.php => OldCsvImporter.php} | 0 ...Interface.php => OldImporterInterface.php} | 0 app/Import/Setup/CsvSetup.php | 1 - app/Models/ImportJob.php | 8 +- .../ImportJob/ImportJobRepository.php | 79 ++- .../ImportJobRepositoryInterface.php | 17 + .../Configuration/ConfigurationInterface.php | 46 ++ .../Import/Configuration/Csv/Initial.php | 122 ++++ app/Support/Import/Configuration/Csv/Map.php | 229 +++++++ .../Import/Configuration/Csv/Roles.php | 261 ++++++++ config/csv.php | 1 + config/firefly.php | 5 +- public/js/ff/import/status.js | 103 +++- resources/lang/en_US/csv.php | 46 +- resources/lang/en_US/firefly.php | 1 - .../csv/{configure.twig => initial.twig} | 47 +- resources/views/import/csv/map.twig | 5 +- resources/views/import/csv/roles.twig | 36 +- resources/views/import/index.twig | 5 +- resources/views/import/status.twig | 55 +- routes/web.php | 5 +- 25 files changed, 1415 insertions(+), 423 deletions(-) create mode 100644 app/Import/Configurator/ConfiguratorInterface.php create mode 100644 app/Import/Configurator/CsvConfigurator.php rename app/Import/Importer/{CsvImporter.php => OldCsvImporter.php} (100%) rename app/Import/Importer/{ImporterInterface.php => OldImporterInterface.php} (100%) create mode 100644 app/Support/Import/Configuration/ConfigurationInterface.php create mode 100644 app/Support/Import/Configuration/Csv/Initial.php create mode 100644 app/Support/Import/Configuration/Csv/Map.php create mode 100644 app/Support/Import/Configuration/Csv/Roles.php rename resources/views/import/csv/{configure.twig => initial.twig} (61%) diff --git a/app/Http/Controllers/ImportController.php b/app/Http/Controllers/ImportController.php index 325698f720..a9fb1491b4 100644 --- a/app/Http/Controllers/ImportController.php +++ b/app/Http/Controllers/ImportController.php @@ -12,11 +12,10 @@ declare(strict_types=1); namespace FireflyIII\Http\Controllers; -use Crypt; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Http\Requests\ImportUploadRequest; +use FireflyIII\Import\Configurator\ConfiguratorInterface; use FireflyIII\Import\ImportProcedureInterface; -use FireflyIII\Import\Setup\SetupInterface; use FireflyIII\Models\ImportJob; use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; use FireflyIII\Repositories\Tag\TagRepositoryInterface; @@ -25,10 +24,6 @@ use Illuminate\Http\Request; use Illuminate\Http\Response as LaravelResponse; use Log; use Response; -use Session; -use SplFileObject; -use Storage; -use Symfony\Component\HttpFoundation\File\UploadedFile; use View; /** @@ -54,30 +49,28 @@ class ImportController extends Controller } ); } + // + // /** + // * This is the last step before the import starts. + // * + // * @param ImportJob $job + // * + // * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|View + // */ + // public function complete(ImportJob $job) + // { + // Log::debug('Now in complete()', ['job' => $job->key]); + // if (!$this->jobInCorrectStep($job, 'complete')) { + // return $this->redirectToCorrectStep($job); + // } + // $subTitle = trans('firefly.import_complete'); + // $subTitleIcon = 'fa-star'; + // + // return view('import.complete', compact('job', 'subTitle', 'subTitleIcon')); + // } /** - * This is the last step before the import starts. - * - * @param ImportJob $job - * - * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|View - */ - public function complete(ImportJob $job) - { - Log::debug('Now in complete()', ['job' => $job->key]); - if (!$this->jobInCorrectStep($job, 'complete')) { - return $this->redirectToCorrectStep($job); - } - $subTitle = trans('firefly.import_complete'); - $subTitleIcon = 'fa-star'; - - return view('import.complete', compact('job', 'subTitle', 'subTitleIcon')); - } - - /** - * This is step 3. - * This is the first step in configuring the job. It can only be executed - * when the job is set to "import_status_never_started". + * This is step 3. This repeats until the job is configured. * * @param ImportJob $job * @@ -86,19 +79,19 @@ class ImportController extends Controller */ public function configure(ImportJob $job) { - Log::debug('Now at start of configure()'); - if (!$this->jobInCorrectStep($job, 'configure')) { - return $this->redirectToCorrectStep($job); - } + // create configuration class: + $configurator = $this->makeConfigurator($job); - // actual code - $importer = $this->makeImporter($job); - $importer->configure(); - $data = $importer->getConfigurationData(); + // is the job already configured? + if ($configurator->isJobConfigured()) { + return redirect(route('import.status', [$job->key])); + } + $view = $configurator->getNextView(); + $data = $configurator->getNextData(); $subTitle = trans('firefly.configure_import'); $subTitleIcon = 'fa-wrench'; - return view('import.' . $job->file_type . '.configure', compact('data', 'job', 'subTitle', 'subTitleIcon')); + return view($view, compact('data', 'job', 'subTitle', 'subTitleIcon')); } /** @@ -134,25 +127,25 @@ class ImportController extends Controller } - /** - * @param ImportJob $job - * - * @return View - */ - public function finished(ImportJob $job) - { - if (!$this->jobInCorrectStep($job, 'finished')) { - return $this->redirectToCorrectStep($job); - } - - // if there is a tag (there might not be), we can link to it: - $tagId = $job->extended_status['importTag'] ?? 0; - - $subTitle = trans('firefly.import_finished'); - $subTitleIcon = 'fa-star'; - - return view('import.finished', compact('job', 'subTitle', 'subTitleIcon', 'tagId')); - } + // /** + // * @param ImportJob $job + // * + // * @return View + // */ + // public function finished(ImportJob $job) + // { + // if (!$this->jobInCorrectStep($job, 'finished')) { + // return $this->redirectToCorrectStep($job); + // } + // + // // if there is a tag (there might not be), we can link to it: + // $tagId = $job->extended_status['importTag'] ?? 0; + // + // $subTitle = trans('firefly.import_finished'); + // $subTitleIcon = 'fa-star'; + // + // return view('import.finished', compact('job', 'subTitle', 'subTitleIcon', 'tagId')); + // } /** * This is step 1. Upload a file. @@ -161,7 +154,6 @@ class ImportController extends Controller */ public function index() { - Log::debug('Now at index'); $subTitle = trans('firefly.import_data_index'); $subTitleIcon = 'fa-home'; $importFileTypes = []; @@ -174,6 +166,35 @@ class ImportController extends Controller return view('import.index', compact('subTitle', 'subTitleIcon', 'importFileTypes', 'defaultImportType')); } + /** + * This is step 2. It creates an Import Job. Stores the import. + * + * @param ImportUploadRequest $request + * @param ImportJobRepositoryInterface $repository + * @param UserRepositoryInterface $userRepository + * + * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + */ + public function initialize(ImportUploadRequest $request, ImportJobRepositoryInterface $repository, UserRepositoryInterface $userRepository) + { + Log::debug('Now in initialize()'); + + // create import job: + $type = $request->get('import_file_type'); + $job = $repository->create($type); + Log::debug('Created new job', ['key' => $job->key, 'id' => $job->id]); + + // process file: + $repository->processFile($job, $request->files->get('import_file')); + + // process config, if present: + if ($request->files->has('configuration_file')) { + $repository->processConfiguration($job, $request->files->get('configuration_file')); + } + + return redirect(route('import.configure', [$job->key])); + } + /** * @param ImportJob $job * @@ -182,22 +203,22 @@ class ImportController extends Controller public function json(ImportJob $job) { $result = [ - 'showPercentage' => false, - 'started' => false, - 'finished' => false, - 'running' => false, - 'errors' => $job->extended_status['errors'], - 'percentage' => 0, - 'steps' => $job->extended_status['total_steps'], - 'stepsDone' => $job->extended_status['steps_done'], - 'statusText' => trans('firefly.import_status_' . $job->status), - 'finishedText' => '', + 'started' => false, + 'finished' => false, + 'running' => false, + 'errors' => $job->extended_status['errors'], + 'percentage' => 0, + 'steps' => $job->extended_status['total_steps'], + 'stepsDone' => $job->extended_status['steps_done'], + 'statusText' => trans('firefly.import_status_' . $job->status), + 'status' => $job->status, + 'finishedText' => '', ]; $percentage = 0; if ($job->extended_status['total_steps'] !== 0) { $percentage = round(($job->extended_status['steps_done'] / $job->extended_status['total_steps']) * 100, 0); } - if ($job->status === 'import_complete') { + if ($job->status === 'complete') { $tagId = $job->extended_status['importTag']; /** @var TagRepositoryInterface $repository */ $repository = app(TagRepositoryInterface::class); @@ -206,11 +227,10 @@ class ImportController extends Controller $result['finishedText'] = trans('firefly.import_finished_link', ['link' => route('tags.show', [$tag->id]), 'tag' => $tag->tag]); } - if ($job->status === 'import_running') { - $result['started'] = true; - $result['running'] = true; - $result['showPercentage'] = true; - $result['percentage'] = $percentage; + if ($job->status === 'running') { + $result['started'] = true; + $result['running'] = true; + $result['percentage'] = $percentage; } return Response::json($result); @@ -228,87 +248,81 @@ class ImportController extends Controller public function postConfigure(Request $request, ImportJobRepositoryInterface $repository, ImportJob $job) { Log::debug('Now in postConfigure()', ['job' => $job->key]); - if (!$this->jobInCorrectStep($job, 'process')) { - return $this->redirectToCorrectStep($job); + $configurator = $this->makeConfigurator($job); + + // is the job already configured? + if ($configurator->isJobConfigured()) { + return redirect(route('import.status', [$job->key])); } - Log::debug('Continue postConfigure()', ['job' => $job->key]); + $data = $request->all(); + $configurator->configureJob($data); - // actual code - $importer = $this->makeImporter($job); - $data = $request->all(); - $files = $request->files; - $importer->saveImportConfiguration($data, $files); - - // update job: - $repository->updateStatus($job, 'import_configuration_saved'); - - // return redirect to settings. - // this could loop until the user is done. - return redirect(route('import.settings', [$job->key])); + // return to configure + return redirect(route('import.configure', [$job->key])); } - /** - * This step 6. Depending on the importer, this will process the - * settings given and store them. - * - * @param Request $request - * @param ImportJob $job - * - * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector - * @throws FireflyException - */ - public function postSettings(Request $request, ImportJob $job) - { - Log::debug('Now in postSettings()', ['job' => $job->key]); - if (!$this->jobInCorrectStep($job, 'store-settings')) { - return $this->redirectToCorrectStep($job); - } - $importer = $this->makeImporter($job); - $importer->storeSettings($request); + // /** + // * This step 6. Depending on the importer, this will process the + // * settings given and store them. + // * + // * @param Request $request + // * @param ImportJob $job + // * + // * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + // * @throws FireflyException + // */ + // public function postSettings(Request $request, ImportJob $job) + // { + // Log::debug('Now in postSettings()', ['job' => $job->key]); + // if (!$this->jobInCorrectStep($job, 'store-settings')) { + // return $this->redirectToCorrectStep($job); + // } + // $importer = $this->makeImporter($job); + // $importer->storeSettings($request); + // + // // return redirect to settings (for more settings perhaps) + // return redirect(route('import.settings', [$job->key])); + // } - // return redirect to settings (for more settings perhaps) - return redirect(route('import.settings', [$job->key])); - } - - /** - * Step 5. Depending on the importer, this will show the user settings to - * fill in. - * - * @param ImportJobRepositoryInterface $repository - * @param ImportJob $job - * - * @return View - */ - public function settings(ImportJobRepositoryInterface $repository, ImportJob $job) - { - Log::debug('Now in settings()', ['job' => $job->key]); - if (!$this->jobInCorrectStep($job, 'settings')) { - return $this->redirectToCorrectStep($job); - } - Log::debug('Continue in settings()'); - $importer = $this->makeImporter($job); - $subTitle = trans('firefly.settings_for_import'); - $subTitleIcon = 'fa-wrench'; - - // now show settings screen to user. - if ($importer->requireUserSettings()) { - Log::debug('Job requires user config.'); - $data = $importer->getDataForSettings(); - $view = $importer->getViewForSettings(); - - return view($view, compact('data', 'job', 'subTitle', 'subTitleIcon')); - } - Log::debug('Job does NOT require user config.'); - - $repository->updateStatus($job, 'settings_complete'); - - // if no more settings, save job and continue to process thing. - return redirect(route('import.complete', [$job->key])); - - // ask the importer for the requested action. - // for example pick columns or map data. - // depends of course on the data in the job. - } + // /** + // * Step 5. Depending on the importer, this will show the user settings to + // * fill in. + // * + // * @param ImportJobRepositoryInterface $repository + // * @param ImportJob $job + // * + // * @return View + // */ + // public function settings(ImportJobRepositoryInterface $repository, ImportJob $job) + // { + // Log::debug('Now in settings()', ['job' => $job->key]); + // if (!$this->jobInCorrectStep($job, 'settings')) { + // return $this->redirectToCorrectStep($job); + // } + // Log::debug('Continue in settings()'); + // $importer = $this->makeImporter($job); + // $subTitle = trans('firefly.settings_for_import'); + // $subTitleIcon = 'fa-wrench'; + // + // // now show settings screen to user. + // if ($importer->requireUserSettings()) { + // Log::debug('Job requires user config.'); + // $data = $importer->getDataForSettings(); + // $view = $importer->getViewForSettings(); + // + // return view($view, compact('data', 'job', 'subTitle', 'subTitleIcon')); + // } + // Log::debug('Job does NOT require user config.'); + // + // $repository->updateStatus($job, 'settings_complete'); + // + // // if no more settings, save job and continue to process thing. + // return redirect(route('import.complete', [$job->key])); + // + // // ask the importer for the requested action. + // // for example pick columns or map data. + // // depends of course on the data in the job. + // } /** * @param ImportProcedureInterface $importProcedure @@ -316,6 +330,7 @@ class ImportController extends Controller */ public function start(ImportProcedureInterface $importProcedure, ImportJob $job) { + die('TODO here.'); set_time_limit(0); if ($job->status == 'settings_complete') { $importProcedure->runImport($job); @@ -330,175 +345,117 @@ class ImportController extends Controller * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|View */ public function status(ImportJob $job) - { // - Log::debug('Now in status()', ['job' => $job->key]); - if (!$this->jobInCorrectStep($job, 'status')) { - return $this->redirectToCorrectStep($job); - } + { $subTitle = trans('firefly.import_status'); $subTitleIcon = 'fa-star'; return view('import.status', compact('job', 'subTitle', 'subTitleIcon')); } - - /** - * This is step 2. It creates an Import Job. Stores the import. - * - * @param ImportUploadRequest $request - * @param ImportJobRepositoryInterface $repository - * @param UserRepositoryInterface $userRepository - * - * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector - */ - public function upload(ImportUploadRequest $request, ImportJobRepositoryInterface $repository, UserRepositoryInterface $userRepository) - { - Log::debug('Now in upload()'); - // create import job: - $type = $request->get('import_file_type'); - $job = $repository->create($type); - Log::debug('Created new job', ['key' => $job->key, 'id' => $job->id]); - - /** @var UploadedFile $upload */ - $upload = $request->files->get('import_file'); - $newName = $job->key . '.upload'; - $uploaded = new SplFileObject($upload->getRealPath()); - $content = $uploaded->fread($uploaded->getSize()); - $contentEncrypted = Crypt::encrypt($content); - $disk = Storage::disk('upload'); - - // user is demo user, replace upload with prepared file. - if ($userRepository->hasRole(auth()->user(), 'demo')) { - $stubsDisk = Storage::disk('stubs'); - $content = $stubsDisk->get('demo-import.csv'); - $contentEncrypted = Crypt::encrypt($content); - $disk->put($newName, $contentEncrypted); - Log::debug('Replaced upload with demo file.'); - - // also set up prepared configuration. - $configuration = json_decode($stubsDisk->get('demo-configuration.json'), true); - $repository->setConfiguration($job, $configuration); - Log::debug('Set configuration for demo user', $configuration); - - // also flash info - Session::flash('info', trans('demo.import-configure-security')); - } - if (!$userRepository->hasRole(auth()->user(), 'demo')) { - // user is not demo, process original upload: - $disk->put($newName, $contentEncrypted); - Log::debug('Uploaded file', ['name' => $upload->getClientOriginalName(), 'size' => $upload->getSize(), 'mime' => $upload->getClientMimeType()]); - } - - // store configuration file's content into the job's configuration thing. Otherwise, leave it empty. - // demo user's configuration upload is ignored completely. - if ($request->files->has('configuration_file') && !auth()->user()->hasRole('demo')) { - /** @var UploadedFile $configFile */ - $configFile = $request->files->get('configuration_file'); - Log::debug( - 'Uploaded configuration file', - ['name' => $configFile->getClientOriginalName(), 'size' => $configFile->getSize(), 'mime' => $configFile->getClientMimeType()] - ); - - $configFileObject = new SplFileObject($configFile->getRealPath()); - $configRaw = $configFileObject->fread($configFileObject->getSize()); - $configuration = json_decode($configRaw, true); - - // @codeCoverageIgnoreStart - if (!is_null($configuration) && is_array($configuration)) { - Log::debug('Found configuration', $configuration); - $repository->setConfiguration($job, $configuration); - } - // @codeCoverageIgnoreEnd - } - - return redirect(route('import.configure', [$job->key])); - } - - /** - * @param ImportJob $job - * @param string $method - * - * @return bool - */ - private function jobInCorrectStep(ImportJob $job, string $method): bool - { - Log::debug('Now in jobInCorrectStep()', ['job' => $job->key, 'method' => $method]); - switch ($method) { - case 'configure': - case 'process': - return $job->status === 'import_status_never_started'; - case 'settings': - case 'store-settings': - Log::debug(sprintf('Job %d with key %s has status %s', $job->id, $job->key, $job->status)); - - return $job->status === 'import_configuration_saved'; - case 'finished': - return $job->status === 'import_complete'; - case 'complete': - return $job->status === 'settings_complete'; - case 'status': - return ($job->status === 'settings_complete') || ($job->status === 'import_running'); - } - - return false; // @codeCoverageIgnore - - } + // /** + // * @param ImportJob $job + // * @param string $method + // * + // * @return bool + // */ + // private function jobInCorrectStep(ImportJob $job, string $method): bool + // { + // Log::debug('Now in jobInCorrectStep()', ['job' => $job->key, 'method' => $method]); + // switch ($method) { + // case 'configure': + // case 'process': + // return $job->status === 'import_status_never_started'; + // case 'settings': + // case 'store-settings': + // Log::debug(sprintf('Job %d with key %s has status %s', $job->id, $job->key, $job->status)); + // + // return $job->status === 'import_configuration_saved'; + // case 'finished': + // return $job->status === 'import_complete'; + // case 'complete': + // return $job->status === 'settings_complete'; + // case 'status': + // return ($job->status === 'settings_complete') || ($job->status === 'import_running'); + // } + // + // return false; // @codeCoverageIgnore + // + // } /** * @param ImportJob $job * - * @return SetupInterface + * @return ConfiguratorInterface * @throws FireflyException */ - private function makeImporter(ImportJob $job): SetupInterface + private function makeConfigurator(ImportJob $job): ConfiguratorInterface { - // create proper importer (depends on job) - $type = strtolower($job->file_type); - - // validate type: - $validTypes = array_keys(config('firefly.import_formats')); - - - if (in_array($type, $validTypes)) { - /** @var SetupInterface $importer */ - $importer = app('FireflyIII\Import\Setup\\' . ucfirst($type) . 'Setup'); - $importer->setJob($job); - - return $importer; + $type = $job->file_type; + $key = sprintf('firefly.import_configurators.%s', $type); + $className = config($key); + if (is_null($className)) { + throw new FireflyException('Cannot find configurator class for this job.'); } - throw new FireflyException(sprintf('"%s" is not a valid file type', $type)); // @codeCoverageIgnore + $configurator = new $className($job); + return $configurator; } - /** - * @param ImportJob $job - * - * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector - * @throws FireflyException - */ - private function redirectToCorrectStep(ImportJob $job) - { - Log::debug('Now in redirectToCorrectStep()', ['job' => $job->key]); - switch ($job->status) { - case 'import_status_never_started': - Log::debug('Will redirect to configure()'); + // /** + // * @param ImportJob $job + // * + // * @return SetupInterface + // * @throws FireflyException + // */ + // private function makeImporter(ImportJob $job): SetupInterface + // { + // // create proper importer (depends on job) + // $type = strtolower($job->file_type); + // + // // validate type: + // $validTypes = array_keys(config('firefly.import_formats')); + // + // + // if (in_array($type, $validTypes)) { + // /** @var SetupInterface $importer */ + // $importer = app('FireflyIII\Import\Setup\\' . ucfirst($type) . 'Setup'); + // $importer->setJob($job); + // + // return $importer; + // } + // throw new FireflyException(sprintf('"%s" is not a valid file type', $type)); // @codeCoverageIgnore + // + // } - return redirect(route('import.configure', [$job->key])); - case 'import_configuration_saved': - Log::debug('Will redirect to settings()'); - - return redirect(route('import.settings', [$job->key])); - case 'settings_complete': - Log::debug('Will redirect to complete()'); - - return redirect(route('import.complete', [$job->key])); - case 'import_complete': - Log::debug('Will redirect to finished()'); - - return redirect(route('import.finished', [$job->key])); - } - - throw new FireflyException('Cannot redirect for job state ' . $job->status); // @codeCoverageIgnore - - } + // /** + // * @param ImportJob $job + // * + // * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector + // * @throws FireflyException + // */ + // private function redirectToCorrectStep(ImportJob $job) + // { + // Log::debug('Now in redirectToCorrectStep()', ['job' => $job->key]); + // switch ($job->status) { + // case 'import_status_never_started': + // Log::debug('Will redirect to configure()'); + // + // return redirect(route('import.configure', [$job->key])); + // case 'import_configuration_saved': + // Log::debug('Will redirect to settings()'); + // + // return redirect(route('import.settings', [$job->key])); + // case 'settings_complete': + // Log::debug('Will redirect to complete()'); + // + // return redirect(route('import.complete', [$job->key])); + // case 'import_complete': + // Log::debug('Will redirect to finished()'); + // + // return redirect(route('import.finished', [$job->key])); + // } + // + // throw new FireflyException('Cannot redirect for job state ' . $job->status); // @codeCoverageIgnore + // + // } } diff --git a/app/Http/Requests/ImportUploadRequest.php b/app/Http/Requests/ImportUploadRequest.php index 93065bcada..758ee6164a 100644 --- a/app/Http/Requests/ImportUploadRequest.php +++ b/app/Http/Requests/ImportUploadRequest.php @@ -38,8 +38,9 @@ class ImportUploadRequest extends Request $types = array_keys(config('firefly.import_formats')); return [ - 'import_file' => 'required|file', - 'import_file_type' => 'required|in:' . join(',', $types), + 'import_file' => 'required|file', + 'import_file_type' => 'required|in:' . join(',', $types), + 'configuration_file' => 'file', ]; } diff --git a/app/Import/Configurator/ConfiguratorInterface.php b/app/Import/Configurator/ConfiguratorInterface.php new file mode 100644 index 0000000000..2de21661df --- /dev/null +++ b/app/Import/Configurator/ConfiguratorInterface.php @@ -0,0 +1,60 @@ +job = $job; + if (is_null($this->job->configuration) || count($this->job->configuration) === 0) { + Log::debug(sprintf('Gave import job %s initial configuration.', $this->job->key)); + $this->job->configuration = config('csv.default_config'); + $this->job->save(); + } + } + + /** + * Store any data from the $data array into the job. + * + * @param array $data + * + * @return bool + * @throws FireflyException + */ + public function configureJob(array $data): bool + { + $class = $this->getConfigurationClass(); + + /** @var ConfigurationInterface $object */ + $object = new $class($this->job); + + return $object->storeConfiguration($data); + } + + /** + * Return the data required for the next step in the job configuration. + * + * @return array + * @throws FireflyException + */ + public function getNextData(): array + { + $class = $this->getConfigurationClass(); + + /** @var ConfigurationInterface $object */ + $object = new $class($this->job); + + return $object->getData(); + + } + + /** + * @return string + * @throws FireflyException + */ + public function getNextView(): string + { + if (!$this->job->configuration['initial-config-complete']) { + return 'import.csv.initial'; + } + if (!$this->job->configuration['column-roles-complete']) { + return 'import.csv.roles'; + } + if (!$this->job->configuration['column-mapping-complete']) { + return 'import.csv.map'; + } + + throw new FireflyException('No view for state'); + } + + /** + * @return bool + */ + public function isJobConfigured(): bool + { + if ($this->job->configuration['initial-config-complete'] + && $this->job->configuration['column-roles-complete'] + && $this->job->configuration['column-mapping-complete'] + ) { + $this->job->status = 'configured'; + $this->job->save(); + + return true; + } + + return false; + } + + /** + * @return string + * @throws FireflyException + */ + private function getConfigurationClass(): string + { + $class = false; + switch (true) { + case(!$this->job->configuration['initial-config-complete']): + $class = 'FireflyIII\\Support\\Import\\Configuration\\Csv\\Initial'; + break; + case (!$this->job->configuration['column-roles-complete']): + $class = 'FireflyIII\\Support\\Import\\Configuration\\Csv\\Roles'; + break; + case (!$this->job->configuration['column-mapping-complete']): + $class = 'FireflyIII\\Support\\Import\\Configuration\\Csv\\Map'; + break; + default: + break; + } + + if ($class === false) { + throw new FireflyException('Cannot handle current job state in getConfigurationClass().'); + } + if (!class_exists($class)) { + throw new FireflyException(sprintf('Class %s does not exist in getConfigurationClass().', $class)); + } + + return $class; + } +} \ No newline at end of file diff --git a/app/Import/Importer/CsvImporter.php b/app/Import/Importer/OldCsvImporter.php similarity index 100% rename from app/Import/Importer/CsvImporter.php rename to app/Import/Importer/OldCsvImporter.php diff --git a/app/Import/Importer/ImporterInterface.php b/app/Import/Importer/OldImporterInterface.php similarity index 100% rename from app/Import/Importer/ImporterInterface.php rename to app/Import/Importer/OldImporterInterface.php diff --git a/app/Import/Setup/CsvSetup.php b/app/Import/Setup/CsvSetup.php index e3888e0030..839d2cca65 100644 --- a/app/Import/Setup/CsvSetup.php +++ b/app/Import/Setup/CsvSetup.php @@ -237,7 +237,6 @@ class CsvSetup implements SetupInterface $all = $request->all(); if ($request->get('settings') == 'roles') { $count = $config['column-count']; - $roleSet = 0; // how many roles have been defined $mapSet = 0; // how many columns must be mapped for ($i = 0; $i < $count; $i++) { diff --git a/app/Models/ImportJob.php b/app/Models/ImportJob.php index 66dfc167eb..fb1ae18c15 100644 --- a/app/Models/ImportJob.php +++ b/app/Models/ImportJob.php @@ -42,11 +42,9 @@ class ImportJob extends Model protected $validStatus = [ - 'import_status_never_started', // initial state - 'import_configuration_saved', // import configuration saved. This step is going to be obsolete. - 'settings_complete', // aka: ready for import. - 'import_running', // import currently underway - 'import_complete', // done with everything + 'new', + 'initialized', + 'configured', ]; /** diff --git a/app/Repositories/ImportJob/ImportJobRepository.php b/app/Repositories/ImportJob/ImportJobRepository.php index 6af984a06c..c1f597c346 100644 --- a/app/Repositories/ImportJob/ImportJobRepository.php +++ b/app/Repositories/ImportJob/ImportJobRepository.php @@ -13,10 +13,16 @@ declare(strict_types=1); namespace FireflyIII\Repositories\ImportJob; +use Crypt; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\ImportJob; +use FireflyIII\Repositories\User\UserRepositoryInterface; use FireflyIII\User; use Illuminate\Support\Str; +use Log; +use SplFileObject; +use Storage; +use Symfony\Component\HttpFoundation\File\UploadedFile; /** * Class ImportJobRepository @@ -51,7 +57,7 @@ class ImportJobRepository implements ImportJobRepositoryInterface $importJob->user()->associate($this->user); $importJob->file_type = $fileType; $importJob->key = Str::random(12); - $importJob->status = 'import_status_never_started'; + $importJob->status = 'new'; $importJob->extended_status = [ 'total_steps' => 0, 'steps_done' => 0, @@ -86,6 +92,77 @@ class ImportJobRepository implements ImportJobRepositoryInterface return $result; } + /** + * @param ImportJob $job + * @param UploadedFile $file + * + * @return bool + */ + public function processConfiguration(ImportJob $job, UploadedFile $file): bool + { + /** @var UserRepositoryInterface $repository */ + $repository = app(UserRepositoryInterface::class); + // demo user's configuration upload is ignored completely. + if ($repository->hasRole($this->user, 'demo')) { + Log::debug( + 'Uploaded configuration file', ['name' => $file->getClientOriginalName(), 'size' => $file->getSize(), 'mime' => $file->getClientMimeType()] + ); + + $configFileObject = new SplFileObject($file->getRealPath()); + $configRaw = $configFileObject->fread($configFileObject->getSize()); + $configuration = json_decode($configRaw, true); + + if (!is_null($configuration) && is_array($configuration)) { + Log::debug('Found configuration', $configuration); + $this->setConfiguration($job, $configuration); + } + } + + return true; + } + + /** + * @param ImportJob $job + * @param UploadedFile $file + * + * @return mixed + */ + public function processFile(ImportJob $job, UploadedFile $file): bool + { + /** @var UserRepositoryInterface $repository */ + $repository = app(UserRepositoryInterface::class); + $newName = sprintf('%s.upload', $job->key); + $uploaded = new SplFileObject($file->getRealPath()); + $content = $uploaded->fread($uploaded->getSize()); + $contentEncrypted = Crypt::encrypt($content); + $disk = Storage::disk('upload'); + + + // user is demo user, replace upload with prepared file. + if ($repository->hasRole($this->user, 'demo')) { + $stubsDisk = Storage::disk('stubs'); + $content = $stubsDisk->get('demo-import.csv'); + $contentEncrypted = Crypt::encrypt($content); + $disk->put($newName, $contentEncrypted); + Log::debug('Replaced upload with demo file.'); + + // also set up prepared configuration. + $configuration = json_decode($stubsDisk->get('demo-configuration.json'), true); + $this->setConfiguration($job, $configuration); + Log::debug('Set configuration for demo user', $configuration); + } + + if (!$repository->hasRole($this->user, 'demo')) { + // user is not demo, process original upload: + $disk->put($newName, $contentEncrypted); + Log::debug('Uploaded file', ['name' => $file->getClientOriginalName(), 'size' => $file->getSize(), 'mime' => $file->getClientMimeType()]); + } + $job->status = 'initialized'; + $job->save(); + + return true; + } + /** * @param ImportJob $job * @param array $configuration diff --git a/app/Repositories/ImportJob/ImportJobRepositoryInterface.php b/app/Repositories/ImportJob/ImportJobRepositoryInterface.php index 5bdf636d42..421a7849b2 100644 --- a/app/Repositories/ImportJob/ImportJobRepositoryInterface.php +++ b/app/Repositories/ImportJob/ImportJobRepositoryInterface.php @@ -15,6 +15,7 @@ namespace FireflyIII\Repositories\ImportJob; use FireflyIII\Models\ImportJob; use FireflyIII\User; +use Symfony\Component\HttpFoundation\File\UploadedFile; /** * Interface ImportJobRepositoryInterface @@ -37,6 +38,22 @@ interface ImportJobRepositoryInterface */ public function findByKey(string $key): ImportJob; + /** + * @param ImportJob $job + * @param UploadedFile $file + * + * @return mixed + */ + public function processFile(ImportJob $job, UploadedFile $file): bool; + + /** + * @param ImportJob $job + * @param UploadedFile $file + * + * @return bool + */ + public function processConfiguration(ImportJob $job, UploadedFile $file): bool; + /** * @param ImportJob $job * @param array $configuration diff --git a/app/Support/Import/Configuration/ConfigurationInterface.php b/app/Support/Import/Configuration/ConfigurationInterface.php new file mode 100644 index 0000000000..7cc3140782 --- /dev/null +++ b/app/Support/Import/Configuration/ConfigurationInterface.php @@ -0,0 +1,46 @@ +job = $job; + } + + /** + * @return array + */ + public function getData(): array + { + /** @var AccountRepositoryInterface $accountRepository */ + $accountRepository = app(AccountRepositoryInterface::class); + $accounts = $accountRepository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]); + $delimiters = [ + ',' => trans('form.csv_comma'), + ';' => trans('form.csv_semicolon'), + 'tab' => trans('form.csv_tab'), + ]; + + $specifics = []; + + // collect specifics. + foreach (config('csv.import_specifics') as $name => $className) { + $specifics[$name] = [ + 'name' => $className::getName(), + 'description' => $className::getDescription(), + ]; + } + + $data = [ + 'accounts' => ExpandedForm::makeSelectList($accounts), + 'specifix' => [], + 'delimiters' => $delimiters, + 'specifics' => $specifics, + ]; + + return $data; + } + + /** + * Store the result. + * + * @param array $data + * + * @return bool + */ + public function storeConfiguration(array $data): bool + { + /** @var AccountRepositoryInterface $repository */ + $repository = app(AccountRepositoryInterface::class); + $importId = $data['csv_import_account'] ?? 0; + $account = $repository->find(intval($importId)); + + $hasHeaders = isset($data['has_headers']) && intval($data['has_headers']) === 1 ? true : false; + $config = $this->job->configuration; + $config['initial-config-complete'] = true; + $config['has-headers'] = $hasHeaders; + $config['date-format'] = $data['date_format']; + $config['delimiter'] = $data['csv_delimiter']; + $config['delimiter'] = $config['delimiter'] === 'tab' ? "\t" : $config['delimiter']; + + Log::debug('Entered import account.', ['id' => $importId]); + + + if (!is_null($account->id)) { + Log::debug('Found account.', ['id' => $account->id, 'name' => $account->name]); + $config['import-account'] = $account->id; + } + if (is_null($account->id)) { + Log::error('Could not find anything for csv_import_account.', ['id' => $importId]); + } + + // loop specifics. + if (isset($data['specifics']) && is_array($data['specifics'])) { + foreach ($data['specifics'] as $name => $enabled) { + // verify their content. + $className = sprintf('FireflyIII\Import\Specifics\%s', $name); + if (class_exists($className)) { + $config['specifics'][$name] = 1; + } + } + } + $this->job->configuration = $config; + $this->job->save(); + + return true; + } +} \ No newline at end of file diff --git a/app/Support/Import/Configuration/Csv/Map.php b/app/Support/Import/Configuration/Csv/Map.php new file mode 100644 index 0000000000..f46e4d9188 --- /dev/null +++ b/app/Support/Import/Configuration/Csv/Map.php @@ -0,0 +1,229 @@ +job = $job; + } + + /** + * @return array + * @throws FireflyException + */ + public function getData(): array + { + $config = $this->job->configuration; + $this->getMappableColumns(); + + + // in order to actually map we also need all possible values from the CSV file. + $content = $this->job->uploadFileContents(); + /** @var Reader $reader */ + $reader = Reader::createFromString($content); + $reader->setDelimiter($config['delimiter']); + $results = $reader->fetch(); + $validSpecifics = array_keys(config('csv.import_specifics')); + + foreach ($results as $rowIndex => $row) { + + // skip first row? + if ($rowIndex === 0 && $config['has-headers']) { + continue; + } + + // run specifics here: + // and this is the point where the specifix go to work. + foreach ($config['specifics'] as $name => $enabled) { + + if (!in_array($name, $validSpecifics)) { + throw new FireflyException(sprintf('"%s" is not a valid class name', $name)); + } + $class = config('csv.import_specifics.' . $name); + /** @var SpecificInterface $specific */ + $specific = app($class); + + // it returns the row, possibly modified: + $row = $specific->run($row); + } + + //do something here + foreach ($indexes as $index) { // this is simply 1, 2, 3, etc. + if (!isset($row[$index])) { + // don't really know how to handle this. Just skip, for now. + continue; + } + $value = $row[$index]; + if (strlen($value) > 0) { + + // we can do some preprocessing here, + // which is exclusively to fix the tags: + if (!is_null($data[$index]['preProcessMap'])) { + /** @var PreProcessorInterface $preProcessor */ + $preProcessor = app($data[$index]['preProcessMap']); + $result = $preProcessor->run($value); + $data[$index]['values'] = array_merge($data[$index]['values'], $result); + + Log::debug($rowIndex . ':' . $index . 'Value before preprocessor', ['value' => $value]); + Log::debug($rowIndex . ':' . $index . 'Value after preprocessor', ['value-new' => $result]); + Log::debug($rowIndex . ':' . $index . 'Value after joining', ['value-complete' => $data[$index]['values']]); + + + continue; + } + + $data[$index]['values'][] = $value; + } + } + } + foreach ($data as $index => $entry) { + $data[$index]['values'] = array_unique($data[$index]['values']); + } + + return $data; + } + + /** + * Store the result. + * + * @param array $data + * + * @return bool + */ + public function storeConfiguration(array $data): bool + { + return true; + } + + /** + * @param string $column + * + * @return MapperInterface + */ + private function createMapper(string $column): MapperInterface + { + $mapperClass = config('csv.import_roles.' . $column . '.mapper'); + $mapperName = sprintf('\\FireflyIII\\Import\Mapper\\%s', $mapperClass); + /** @var MapperInterface $mapper */ + $mapper = new $mapperName; + + return $mapper; + } + + /** + * @return bool + */ + private function getMappableColumns(): bool + { + $config = $this->job->configuration; + + /** + * @var int $index + * @var bool $mustBeMapped + */ + foreach ($config['column-do-mapping'] as $index => $mustBeMapped) { + $column = $this->validateColumnName($config['column-roles'][$index] ?? '_ignore'); + $shouldMap = $this->shouldMapColumn($column, $mustBeMapped); + if ($shouldMap) { + + + // create configuration entry for this specific column and add column to $this->data array for later processing. + $this->data[$index] = [ + 'name' => $column, + 'index' => $index, + 'options' => $this->createMapper($column)->getMap(), + 'preProcessMap' => $this->getPreProcessorName($column), + 'values' => [], + ]; + } + } + + return true; + + } + + /** + * @param string $column + * + * @return string + */ + private function getPreProcessorName(string $column): string + { + $name = ''; + $hasPreProcess = config('csv.import_roles.' . $column . '.pre-process-map'); + $preProcessClass = config('csv.import_roles.' . $column . '.pre-process-mapper'); + + if (!is_null($hasPreProcess) && $hasPreProcess === true && !is_null($preProcessClass)) { + $name = sprintf('\\FireflyIII\\Import\\MapperPreProcess\\%s', $preProcessClass); + } + + return $name; + } + + /** + * @param string $column + * @param bool $mustBeMapped + * + * @return bool + */ + private function shouldMapColumn(string $column, bool $mustBeMapped): bool + { + $canBeMapped = config('csv.import_roles.' . $column . '.mappable'); + + return ($canBeMapped && $mustBeMapped); + } + + /** + * @param string $column + * + * @return string + * @throws FireflyException + */ + private function validateColumnName(string $column): string + { + // is valid column? + $validColumns = array_keys(config('csv.import_roles')); + if (!in_array($column, $validColumns)) { + throw new FireflyException(sprintf('"%s" is not a valid column.', $column)); + } + + return $column; + } +} \ No newline at end of file diff --git a/app/Support/Import/Configuration/Csv/Roles.php b/app/Support/Import/Configuration/Csv/Roles.php new file mode 100644 index 0000000000..d439af415e --- /dev/null +++ b/app/Support/Import/Configuration/Csv/Roles.php @@ -0,0 +1,261 @@ +job = $job; + } + + /** + * Get the data necessary to show the configuration screen. + * + * @return array + */ + public function getData(): array + { + $config = $this->job->configuration; + $content = $this->job->uploadFileContents(); + + // create CSV reader. + $reader = Reader::createFromString($content); + $reader->setDelimiter($config['delimiter']); + $start = $config['has-headers'] ? 1 : 0; + $end = $start + config('csv.example_rows'); + + // set data: + $this->data = [ + 'examples' => [], + 'roles' => $this->getRoles(), + 'total' => 0, + 'headers' => $config['has-headers'] ? $reader->fetchOne(0) : [], + ]; + + while ($start < $end) { + $row = $reader->fetchOne($start); + $row = $this->processSpecifics($row); + $count = count($row); + $this->data['total'] = $count > $this->data['total'] ? $count : $this->data['total']; + $this->processRow($row); + $start++; + } + + $this->updateColumCount(); + $this->makeExamplesUnique(); + + return $this->data; + } + + /** + * Store the result. + * + * @param array $data + * + * @return bool + */ + public function storeConfiguration(array $data): bool + { + Log::debug('Now in storeConfiguration of Roles.'); + $config = $this->job->configuration; + $count = $config['column-count']; + for ($i = 0; $i < $count; $i++) { + $role = $data['role'][$i] ?? '_ignore'; + $mapping = isset($data['map'][$i]) && $data['map'][$i] === '1' ? true : false; + $config['column-roles'][$i] = $role; + $config['column-do-mapping'][$i] = $mapping; + Log::debug(sprintf('Column %d has been given role %s', $i, $role)); + } + + + $this->job->configuration = $config; + $this->job->save(); + + $this->ignoreUnmappableColumns(); + $this->setRolesComplete(); + $this->isMappingNecessary(); + + + return true; + } + + /** + * @return array + */ + private function getRoles(): array + { + $roles = []; + foreach (array_keys(config('csv.import_roles')) as $role) { + $roles[$role] = trans('csv.column_' . $role); + } + + return $roles; + + } + + /** + * @return bool + */ + private function ignoreUnmappableColumns(): bool + { + $config = $this->job->configuration; + $count = $config['column-count']; + for ($i = 0; $i < $count; $i++) { + $role = $config['column-roles'][$i] ?? '_ignore'; + $mapping = $config['column-do-mapping'][$i] ?? false; + + if ($role === '_ignore' && $mapping === true) { + $mapping = false; + Log::debug(sprintf('Column %d has type %s so it cannot be mapped.', $i, $role)); + } + $config['column-do-mapping'][$i] = $mapping; + } + + $this->job->configuration = $config; + $this->job->save(); + + return true; + + } + + /** + * @return bool + */ + private function isMappingNecessary() + { + $config = $this->job->configuration; + $count = $config['column-count']; + $toBeMapped = 0; + for ($i = 0; $i < $count; $i++) { + $mapping = $config['column-do-mapping'][$i] ?? false; + if ($mapping === true) { + $toBeMapped++; + } + } + Log::debug(sprintf('Found %d columns that need mapping.', $toBeMapped)); + if ($toBeMapped === 0) { + // skip setting of map, because none need to be mapped: + $config['column-mapping-complete'] = true; + $this->job->configuration = $config; + $this->job->save(); + } + + return true; + } + + /** + * make unique example data + */ + private function makeExamplesUnique(): bool + { + foreach ($this->data['examples'] as $index => $values) { + $this->data['examples'][$index] = array_unique($values); + } + + return true; + } + + /** + * @param array $row + * + * @return bool + */ + private function processRow(array $row): bool + { + foreach ($row as $index => $value) { + $value = trim($value); + if (strlen($value) > 0) { + $this->data['examples'][$index][] = $value; + } + } + + return true; + } + + /** + * run specifics here: + * and this is the point where the specifix go to work. + * + * @param array $row + * + * @return array + */ + private function processSpecifics(array $row): array + { + foreach ($this->job->configuration['specifics'] as $name => $enabled) { + /** @var SpecificInterface $specific */ + $specific = app('FireflyIII\Import\Specifics\\' . $name); + $row = $specific->run($row); + } + + return $row; + } + + /** + * @return bool + */ + private function setRolesComplete(): bool + { + $config = $this->job->configuration; + $count = $config['column-count']; + $assigned = 0; + for ($i = 0; $i < $count; $i++) { + $role = $config['column-roles'][$i] ?? '_ignore'; + if ($role !== '_ignore') { + $assigned++; + } + } + if ($assigned > 0) { + $config['column-roles-complete'] = true; + $this->job->configuration = $config; + $this->job->save(); + } + + return true; + } + + /** + * @return bool + */ + private function updateColumCount(): bool + { + $config = $this->job->configuration; + $count = $this->data['total']; + $config['column-count'] = $count; + $this->job->configuration = $config; + $this->job->save(); + + return true; + } +} \ No newline at end of file diff --git a/config/csv.php b/config/csv.php index 5da40e41e0..cc23de32f1 100644 --- a/config/csv.php +++ b/config/csv.php @@ -292,6 +292,7 @@ return [ // number of example rows: 'example_rows' => 5, 'default_config' => [ + 'initial-config-complete' => false, 'has-headers' => false, // assume 'date-format' => 'Ymd', // assume 'delimiter' => ',', // assume diff --git a/config/firefly.php b/config/firefly.php index c4f648e3a4..e00c6d32d4 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -31,7 +31,10 @@ return [ 'csv' => 'FireflyIII\Export\Exporter\CsvExporter', ], 'import_formats' => [ - 'csv' => 'FireflyIII\Import\Importer\CsvImporter', + 'csv' => 'FireflyIII\Import\Configurator\CsvConfigurator', + ], + 'import_configurators' => [ + 'csv' => 'FireflyIII\Import\Configurator\CsvConfigurator', ], 'default_export_format' => 'csv', 'default_import_format' => 'csv', diff --git a/public/js/ff/import/status.js b/public/js/ff/import/status.js index cc1ec69a9e..75671c2971 100644 --- a/public/js/ff/import/status.js +++ b/public/js/ff/import/status.js @@ -10,6 +10,10 @@ /** global: jobImportUrl, langImportSingleError, langImportMultiError, jobStartUrl, langImportTimeOutError, langImportFinished, langImportFatalError */ +var displayStatus = 'initial'; +var timeOutId; + + var startedImport = false; var startInterval = 2000; var interval = 500; @@ -19,20 +23,85 @@ var stepCount = 0; $(function () { "use strict"; - $('#import-status-intro').hide(); - $('#import-status-more-info').hide(); + //$('#import-status-intro').hide(); + //$('#import-status-more-info').hide(); // check status, every 500 ms. - setTimeout(checkImportStatus, startInterval); + timeOutId = setTimeout(checkImportStatus, startInterval); + + // button to start import routine: + $('.start-job').click(startJob); }); +function startJob() { + console.log('Job started.'); + $.post(jobStartUrl); + return false; +} function checkImportStatus() { "use strict"; $.getJSON(jobImportUrl).done(reportOnJobImport).fail(failedJobImport); } +function reportOnJobImport(data) { + "use strict"; + displayCorrectBox(data.status); + //updateBar(data); + //reportErrors(data); + //reportStatus(data); + //updateTimeout(data); + + //if (importJobFinished(data)) { + // finishedJob(data); + // return; + //} + + + // same number of steps as last time? + //if (currentLimit > timeoutLimit) { + // timeoutError(); + // return; + //} + + // if the job has not actually started, do so now: + //if (!data.started && !startedImport) { + // kickStartJob(); + // return; + //} + + // trigger another check. + //timeOutId = setTimeout(checkImportStatus, interval); + +} + +function displayCorrectBox(status) { + console.log('Current job state is ' + status); + if(status === 'configured' && displayStatus === 'initial') { + // hide some boxes: + $('.status_initial').hide(); + return; + } + console.error('CANNOT HANDLE CURRENT STATE'); +} + + + + + + + + + + + + + + + + + function importComplete() { "use strict"; var bar = $('#import-status-bar'); @@ -131,35 +200,7 @@ function finishedJob(data) { } -function reportOnJobImport(data) { - "use strict"; - updateBar(data); - reportErrors(data); - reportStatus(data); - updateTimeout(data); - if (importJobFinished(data)) { - finishedJob(data); - return; - } - - - // same number of steps as last time? - if (currentLimit > timeoutLimit) { - timeoutError(); - return; - } - - // if the job has not actually started, do so now: - if (!data.started && !startedImport) { - kickStartJob(); - return; - } - - // trigger another check. - setTimeout(checkImportStatus, interval); - -} function startedTheImport() { "use strict"; diff --git a/resources/lang/en_US/csv.php b/resources/lang/en_US/csv.php index d5306e2a88..837616a801 100644 --- a/resources/lang/en_US/csv.php +++ b/resources/lang/en_US/csv.php @@ -13,28 +13,32 @@ declare(strict_types=1); return [ - 'import_configure_title' => 'Configure your import', - 'import_configure_intro' => 'There are some options for your CSV import. Please indicate if your CSV file contains headers on the first column, and what the date format of your date-fields is. That might require some experimentation. The field delimiter is usually a ",", but could also be a ";". Check this carefully.', - 'import_configure_form' => 'Basic CSV import options', - 'header_help' => 'Check this if the first row of your CSV file are the column titles', - 'date_help' => 'Date time format in your CSV. Follow the format like this page indicates. The default value will parse dates that look like this: :dateExample.', - 'delimiter_help' => 'Choose the field delimiter that is used in your input file. If not sure, comma is the safest option.', - 'import_account_help' => 'If your CSV file does NOT contain information about your asset account(s), use this dropdown to select to which account the transactions in the CSV belong to.', - 'upload_not_writeable' => 'The grey box contains a file path. It should be writeable. Please make sure it is.', + // initial config + 'initial_config_title' => 'Import configuration (1/3)', + 'initial_config_text' => 'To be able to import your file correctly, please validate the options below.', + 'initial_config_box' => 'Basic CSV import configuration', + 'initial_header_help' => 'Check this box if the first row of your CSV file are the column titles.', + 'initial_date_help' => 'Date time format in your CSV. Follow the format like this page indicates. The default value will parse dates that look like this: :dateExample.', + 'initial_delimiter_help' => 'Choose the field delimiter that is used in your input file. If not sure, comma is the safest option.', + 'initial_import_account_help' => 'If your CSV file does NOT contain information about your asset account(s), use this dropdown to select to which account the transactions in the CSV belong to.', - // roles - 'column_roles_title' => 'Define column roles', - 'column_roles_table' => 'Table', - 'column_name' => 'Name of column', - 'column_example' => 'Column example data', - 'column_role' => 'Column data meaning', - 'do_map_value' => 'Map these values', - 'column' => 'Column', - 'no_example_data' => 'No example data available', - 'store_column_roles' => 'Continue import', - 'do_not_map' => '(do not map)', - 'map_title' => 'Connect import data to Firefly III data', - 'map_text' => 'In the following tables, the left value shows you information found in your uploaded CSV file. It is your task to map this value, if possible, to a value already present in your database. Firefly will stick to this mapping. If there is no value to map to, or you do not wish to map the specific value, select nothing.', + // roles config + 'roles_title' => 'Define each column\'s role', + 'roles_text' => 'Each column in your CSV file contains certain data. Please indicate what kind of data the importer should expect. The option to "map" data means that you will link each entry found in the column to a value in your database. An often mapped column is the column that contains the IBAN of the opposing account. That can be easily matched to IBAN\'s present in your database already.', + 'roles_table' => 'Table', + 'roles_column_name' => 'Name of column', + 'roles_column_example' => 'Column example data', + 'roles_column_role' => 'Column data meaning', + 'roles_do_map_value' => 'Map these values', + 'roles_column' => 'Column', + 'roles_no_example_data' => 'No example data available', + + 'roles_store' => 'Continue import', + 'roles_do_not_map' => '(do not map)', + + // map data + 'map_title' => 'Connect import data to Firefly III data', + 'map_text' => 'In the following tables, the left value shows you information found in your uploaded CSV file. It is your task to map this value, if possible, to a value already present in your database. Firefly will stick to this mapping. If there is no value to map to, or you do not wish to map the specific value, select nothing.', 'field_value' => 'Field value', 'field_mapped_to' => 'Mapped to', diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index 501c03430a..2755ae6f58 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -1003,7 +1003,6 @@ return [ 'import_finished_report' => 'The import has finished. Please note any errors in the block above this line. All transactions imported during this particular session have been tagged, and you can check them out below. ', 'import_finished_link' => 'The transactions imported can be found in tag :tag.', 'need_at_least_one_account' => 'You need at least one asset account to be able to create piggy banks', - 'see_help_top_right' => 'For more information, please check out the help pages using the icon in the top right corner of the page.', 'bread_crumb_import_complete' => 'Import ":key" complete', 'bread_crumb_configure_import' => 'Configure import ":key"', 'bread_crumb_import_finished' => 'Import ":key" finished', diff --git a/resources/views/import/csv/configure.twig b/resources/views/import/csv/initial.twig similarity index 61% rename from resources/views/import/csv/configure.twig rename to resources/views/import/csv/initial.twig index 9a9e53a0b0..2af4c072b8 100644 --- a/resources/views/import/csv/configure.twig +++ b/resources/views/import/csv/initial.twig @@ -10,11 +10,11 @@

-

{{ trans('csv.import_configure_title') }}

+

{{ trans('csv.initial_config_title') }}

- {{ trans('csv.import_configure_intro') }} + {{ trans('csv.initial_config_text') }}

@@ -29,16 +29,16 @@
-

{{ trans('csv.import_configure_form') }}

+

{{ trans('csv.initial_config_box') }}

- {{ ExpandedForm.checkbox('has_headers',1,job.configuration['has-headers'],{helpText: trans('csv.header_help')}) }} - {{ ExpandedForm.text('date_format',job.configuration['date-format'],{helpText: trans('csv.date_help', {dateExample: phpdate('Ymd')}) }) }} - {{ ExpandedForm.select('csv_delimiter', data.delimiters, job.configuration['delimiter'], {helpText: trans('csv.delimiter_help') } ) }} - {{ ExpandedForm.select('csv_import_account', data.accounts, job.configuration['import-account'], {helpText: trans('csv.import_account_help')} ) }} + {{ ExpandedForm.checkbox('has_headers',1,job.configuration['has-headers'],{helpText: trans('csv.initial_header_help')}) }} + {{ ExpandedForm.text('date_format',job.configuration['date-format'],{helpText: trans('csv.initial_date_help', {dateExample: phpdate('Ymd')}) }) }} + {{ ExpandedForm.select('csv_delimiter', data.delimiters, job.configuration['delimiter'], {helpText: trans('csv.initial_delimiter_help') } ) }} + {{ ExpandedForm.select('csv_import_account', data.accounts, job.configuration['import-account'], {helpText: trans('csv.initial_import_account_help')} ) }} {% for type, specific in data.specifics %}
@@ -56,40 +56,23 @@
{% endfor %} - - {% if not data.is_upload_possible %} -
-
-   -
- -
-
{{ data.upload_path }}
-

- {{ trans('csv.upload_not_writeable') }} -

-
-
- {% endif %}
- {% if data.is_upload_possible %} -
-
-
-
- -
+
+
+
+
+
- {% endif %} +
diff --git a/resources/views/import/csv/map.twig b/resources/views/import/csv/map.twig index ebc0cf669d..3fd6f31d57 100644 --- a/resources/views/import/csv/map.twig +++ b/resources/views/import/csv/map.twig @@ -1,7 +1,7 @@ {% extends "./layout/default" %} {% block breadcrumbs %} - {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName) }} + {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, job) }} {% endblock %} {% block content %} @@ -22,9 +22,8 @@
-
+ - {% for field in data %}
diff --git a/resources/views/import/csv/roles.twig b/resources/views/import/csv/roles.twig index 0df197187b..307498f52c 100644 --- a/resources/views/import/csv/roles.twig +++ b/resources/views/import/csv/roles.twig @@ -1,7 +1,7 @@ {% extends "./layout/default" %} {% block breadcrumbs %} - {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName) }} + {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName, job) }} {% endblock %} {% block content %} @@ -10,18 +10,18 @@
-

{{ trans('csv.column_roles_title') }}

+

{{ trans('csv.roles_title') }}

- {{ 'see_help_top_right'|_ }} + {{ trans('csv.roles_text') }}

- + @@ -29,41 +29,41 @@
-

{{ trans('csv.column_roles_table') }}

+

{{ trans('csv.roles_table') }}

- - - - + + + + - {% for i in 0..(data.columnCount-1) %} + {% for i in 0..(data.total -1) %} @@ -91,7 +91,7 @@
diff --git a/resources/views/import/index.twig b/resources/views/import/index.twig index 922a3fa12e..2f8ff89345 100644 --- a/resources/views/import/index.twig +++ b/resources/views/import/index.twig @@ -23,20 +23,19 @@
-
+ {{ ExpandedForm.file('import_file', {helpText: 'import_file_help'|_}) }} {{ ExpandedForm.file('configuration_file', {helpText: 'configuration_file_help'|_|raw}) }} - {{ ExpandedForm.select('import_file_type', importFileTypes, defaultImportType, {'helpText' : 'import_file_type_help'|_}) }}
-
-
{{ trans('csv.column_name') }}{{ trans('csv.column_example') }}{{ trans('csv.column_role') }}{{ trans('csv.do_map_value') }}{{ trans('csv.roles_column_name') }}{{ trans('csv.roles_column_example') }}{{ trans('csv.roles_column_role') }}{{ trans('csv.roles_do_map_value') }}
- {% if data.columnHeaders[i] == '' %} - {{ trans('csv.column') }} #{{ loop.index }} + {% if data.headers[i] == '' %} + {{ trans('csv.roles_column') }} #{{ loop.index }} {% else %} - {{ data.columnHeaders[i] }} + {{ data.headers[i] }} {% endif %} - {% if data.columns[i]|length == 0 %} - {{ trans('csv.no_example_data') }} + {% if data.examples[i]|length == 0 %} + {{ trans('csv.roles_no_example_data') }} {% else %} - {% for example in data.columns[i] %} + {% for example in data.examples[i] %} {{ example }}
{% endfor %} {% endif %}
{{ Form.select(('role['~loop.index0~']'), - data.available_roles, + data.roles, job.configuration['column-roles'][loop.index0], {class: 'form-control'}) }}
+
- - - - - + + + + + @@ -151,7 +151,7 @@ - - - - @@ -210,17 +213,22 @@ {% endif %} {% endblock %} - {% block scripts %} - +{% block styles %} + +{% endblock %} - - {% endblock %} +{% block scripts %} + + + +{% endblock %} From 2bf54d0b8ee089f2f5aede412016d924e1e915f1 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 14 Jul 2017 17:10:56 +0200 Subject: [PATCH 117/126] Remove extra X [skip ci] --- resources/views/form/amount.twig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/resources/views/form/amount.twig b/resources/views/form/amount.twig index eaf1ec2d8f..c28227d1f7 100644 --- a/resources/views/form/amount.twig +++ b/resources/views/form/amount.twig @@ -7,7 +7,7 @@
{{ 'budget'|_ }}{{ 'budgeted'|_ }}{{ 'budget'|_ }}{{ 'budgeted'|_ }}
+ {% if budgetInformation[budget.id]['currentLimit'] %} {{ budget.name }} {% endif %} + {% if budgetInformation[budget.id]['currentLimit'] %} + {% set repAmount = budgetInformation[budget.id]['currentLimit'].amount %} + {% else %} + {% set repAmount = '0' %} + {% endif %} + +
{{ defaultCurrency.symbol|raw }}
- {% if budgetInformation[budget.id]['currentLimit'] %} - {% set repAmount = budgetInformation[budget.id]['currentLimit'].amount %} - {% else %} - {% set repAmount = '0' %} - {% endif %}
- - - - - - - - - - - {% for account in result.accounts %} - - - - - - - - {% endfor %} - - -
{{ trans('list.name') }}
- {{ account.name }} - {{ trans('firefly.'~account.accountType.type) }}
diff --git a/resources/views/search/partials/budgets.twig b/resources/views/search/partials/budgets.twig deleted file mode 100644 index 5d6c4008d4..0000000000 --- a/resources/views/search/partials/budgets.twig +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - {% for budget in result.budgets %} - - - - - {% endfor %} - - -
{{ trans('list.name') }}
- {{ budget.name }} -
diff --git a/resources/views/search/partials/categories.twig b/resources/views/search/partials/categories.twig deleted file mode 100644 index 7dfef5da15..0000000000 --- a/resources/views/search/partials/categories.twig +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - {% for category in result.categories %} - - - - - {% endfor %} - - -
{{ trans('list.name') }}
- {{ category.name }} -
diff --git a/resources/views/search/partials/tags.twig b/resources/views/search/partials/tags.twig deleted file mode 100644 index fd4342c67b..0000000000 --- a/resources/views/search/partials/tags.twig +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - {% for tag in result.tags %} - - - - - - {% endfor %} - - -
{{ trans('list.name') }}{{ trans('list.type') }}
- {{ tag.tag }} - {{ ('tag'~tag.tagMode)|_ }}
diff --git a/resources/views/search/partials/transactions-large.twig b/resources/views/search/partials/transactions-large.twig deleted file mode 100644 index ca8c06ac92..0000000000 --- a/resources/views/search/partials/transactions-large.twig +++ /dev/null @@ -1,130 +0,0 @@ -{{ journals.render|raw }} - - - - - - - - - - - - - {% if not hideBudgets %} - - {% endif %} - - - {% if not hideCategories %} - - {% endif %} - - - {% if not hideBills %} - - {% endif %} - - - - {% for transaction in journals %} - - - - - - - - - - - - {% if not hideBudgets %} - - {% endif %} - - - {% if not hideCategories %} - - {% endif %} - - - {% if not hideBills %} - - {% endif %} - - {% endfor %} - -
{{ trans('list.description') }}{{ trans('list.amount') }}
- - - {% if transaction.transaction_description|length > 0 %} - {{ transaction.transaction_description }} ({{ transaction.description }}) - {% else %} - {{ transaction.description }} - {% endif %} - - {{ splitJournalIndicator(transaction.journal_id) }} - - {% if transaction.transactionJournal.attachments|length > 0 %} - - {% endif %} - - - - {# TODO replace with new format code #} - XX.XX - - -
- -
-
- {{ journals.render|raw }} -
-
- diff --git a/resources/views/search/partials/transactions.twig b/resources/views/search/partials/transactions.twig deleted file mode 100644 index 387b5ec07b..0000000000 --- a/resources/views/search/partials/transactions.twig +++ /dev/null @@ -1,78 +0,0 @@ -{{ journals.render|raw }} - - - - - - - - - - - - {% for transaction in transactions %} - - - - - - - - {% endfor %} - -
{{ trans('list.description') }}{{ trans('list.amount') }}
- - - {% if transaction.transaction_description|length > 0 %} - {{ transaction.transaction_description }} ({{ transaction.description }}) - {% else %} - {{ transaction.description }} - {% endif %} - - {{ splitJournalIndicator(transaction.journal_id) }} - - {% if transaction.transactionJournal.attachments|length > 0 %} - - {% endif %} - - - {# TODO replace with new format code #} - XX.XX - - -
- -
-
- {{ journals.render|raw }} -
-
- diff --git a/resources/views/search/search.twig b/resources/views/search/search.twig new file mode 100644 index 0000000000..61e89915b4 --- /dev/null +++ b/resources/views/search/search.twig @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + {% for transaction in transactions %} + + + + + + + + + + + + + {% endfor %} + +
{{ trans('list.description') }}{{ trans('list.amount') }}
+ + + {% if transaction.transaction_description|length > 0 %} + {{ transaction.transaction_description }} ({{ transaction.description }}) + {% else %} + {{ transaction.description }} + {% endif %} + + {{ splitJournalIndicator(transaction.journal_id) }} + + {% if transaction.transactionJournal.attachments|length > 0 %} + + {% endif %} + + + {{ transactionAmount(transaction) }} + +
\ No newline at end of file diff --git a/routes/web.php b/routes/web.php index 27724470df..709e58bbe3 100755 --- a/routes/web.php +++ b/routes/web.php @@ -641,7 +641,7 @@ Route::group( Route::group( ['middleware' => 'user-full-auth', 'prefix' => 'search', 'as' => 'search.'], function () { Route::get('', ['uses' => 'SearchController@index', 'as' => 'index']); - + Route::any('search', ['uses' => 'SearchController@search', 'as' => 'search']); } ); From 8a38ce1964692c4f892d11001fb2e8babd847a52 Mon Sep 17 00:00:00 2001 From: James Cole Date: Sat, 15 Jul 2017 21:40:42 +0200 Subject: [PATCH 126/126] Remove references to old tour but include code for new tour. --- app/Http/Controllers/Controller.php | 8 ++++ app/Http/Controllers/HomeController.php | 29 ++++++++++++- app/Http/Controllers/JsonController.php | 38 ----------------- app/Http/Controllers/NewUserController.php | 47 +-------------------- app/Support/Amount.php | 3 ++ public/css/bootstrap-tour.min.css | 22 ---------- public/js/ff/index.js | 23 +--------- public/js/ff/intro/intro.js | 14 +++++++ public/js/lib/bootstrap-tour.min.js | 22 ---------- public/lib/intro/intro.min.js | 49 ++++++++++++++++++++++ public/lib/intro/introjs-rtl.min.css | 1 + public/lib/intro/introjs.min.css | 1 + resources/lang/en_US/firefly.php | 21 +++------- resources/views/index.twig | 22 +++------- resources/views/json/tour.twig | 15 ------- resources/views/layout/default.twig | 22 ++++++---- resources/views/new-user/index.twig | 12 +++--- routes/web.php | 4 +- 18 files changed, 139 insertions(+), 214 deletions(-) delete mode 100755 public/css/bootstrap-tour.min.css create mode 100644 public/js/ff/intro/intro.js delete mode 100755 public/js/lib/bootstrap-tour.min.js create mode 100755 public/lib/intro/intro.min.js create mode 100755 public/lib/intro/introjs-rtl.min.css create mode 100755 public/lib/intro/introjs.min.css delete mode 100644 resources/views/json/tour.twig diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 63cbe9c975..d46c485086 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -18,6 +18,7 @@ use FireflyIII\Models\AccountType; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; +use FireflyIII\Support\Facades\Preferences; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Bus\DispatchesJobs; use Illuminate\Foundation\Validation\ValidatesRequests; @@ -25,6 +26,7 @@ use Illuminate\Routing\Controller as BaseController; use Session; use URL; use View; +use Route; /** * Class Controller @@ -63,6 +65,12 @@ class Controller extends BaseController $this->monthAndDayFormat = (string)trans('config.month_and_day'); $this->dateTimeFormat = (string)trans('config.date_time'); + // get shown-intro-preference: + $key = 'shown_demo_' . Route::currentRouteName(); + $shownDemo = Preferences::get($key, false)->data; + View::share('shownDemo', $shownDemo); + View::share('current_route_name', Route::currentRouteName()); + return $next($request); } ); diff --git a/app/Http/Controllers/HomeController.php b/app/Http/Controllers/HomeController.php index d52e8e18f7..6ea0eb2119 100644 --- a/app/Http/Controllers/HomeController.php +++ b/app/Http/Controllers/HomeController.php @@ -21,9 +21,11 @@ use FireflyIII\Models\AccountType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Bill\BillRepositoryInterface; use Illuminate\Http\Request; +use Illuminate\Routing\Route; use Illuminate\Support\Collection; use Log; use Preferences; +use Route as RouteFacade; use Session; use View; @@ -120,7 +122,6 @@ class HomeController extends Controller $start = session('start', Carbon::now()->startOfMonth()); /** @var Carbon $end */ $end = session('end', Carbon::now()->endOfMonth()); - $showTour = Preferences::get('tour', true)->data; $accounts = $repository->getAccountsById($frontPage->data); $showDepositsFrontpage = Preferences::get('showDepositsFrontpage', false)->data; @@ -137,10 +138,34 @@ class HomeController extends Controller } return view( - 'index', compact('count', 'showTour', 'title', 'subTitle', 'mainTitleIcon', 'transactions', 'showDepositsFrontpage', 'billCount') + 'index', compact('count', 'title', 'subTitle', 'mainTitleIcon', 'transactions', 'showDepositsFrontpage', 'billCount') ); } + public function routes() + { + $set = RouteFacade::getRoutes(); + $ignore = ['chart.','javascript.','json.','report-data.','popup.','debugbar.']; + /** @var Route $route */ + foreach ($set as $route) { + $name = $route->getName(); + if (!is_null($name) && in_array('GET', $route->methods()) && strlen($name) > 0) { + $found = false; + foreach ($ignore as $string) { + if (strpos($name, $string) !== false) { + $found = true; + } + } + if (!$found) { + echo 'touch '.$route->getName() . '.md;'; + } + + } + } + + return ' '; + } + /** * @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector */ diff --git a/app/Http/Controllers/JsonController.php b/app/Http/Controllers/JsonController.php index bac5c2492f..378c5ebac2 100644 --- a/app/Http/Controllers/JsonController.php +++ b/app/Http/Controllers/JsonController.php @@ -237,16 +237,6 @@ class JsonController extends Controller return Response::json($return); } - /** - * @return \Illuminate\Http\JsonResponse - */ - public function endTour() - { - Preferences::set('tour', false); - - return Response::json('true'); - } - /** * Returns a JSON list of all beneficiaries. * @@ -293,34 +283,6 @@ class JsonController extends Controller } - /** - * - */ - public function tour() - { - $pref = Preferences::get('tour', true); - if (!$pref) { - throw new FireflyException('Cannot find preference for tour. Exit.'); // @codeCoverageIgnore - } - $headers = ['main-content', 'sidebar-toggle', 'account-menu', 'budget-menu', 'report-menu', 'transaction-menu', 'option-menu', 'main-content-end']; - $steps = []; - foreach ($headers as $header) { - $steps[] = [ - 'element' => '#' . $header, - 'title' => trans('help.' . $header . '-title'), - 'content' => trans('help.' . $header . '-text'), - ]; - } - $steps[0]['orphan'] = true;// orphan and backdrop for first element. - $steps[0]['backdrop'] = true; - $steps[1]['placement'] = 'left';// sidebar position left: - $steps[7]['orphan'] = true; // final in the center again. - $steps[7]['backdrop'] = true; - $template = view('json.tour')->render(); - - return Response::json(['steps' => $steps, 'template' => $template]); - } - /** * @param JournalCollectorInterface $collector * @param string $what diff --git a/app/Http/Controllers/NewUserController.php b/app/Http/Controllers/NewUserController.php index 024076c1ef..8e47d8a13b 100644 --- a/app/Http/Controllers/NewUserController.php +++ b/app/Http/Controllers/NewUserController.php @@ -54,7 +54,6 @@ class NewUserController extends Controller View::share('title', trans('firefly.welcome')); View::share('mainTitleIcon', 'fa-fire'); - $types = config('firefly.accountTypesByIdentifier.asset'); $count = $repository->count($types); @@ -74,30 +73,13 @@ class NewUserController extends Controller */ public function submit(NewUserFormRequest $request, AccountRepositoryInterface $repository) { - $count = 1; // create normal asset account: $this->createAssetAccount($request, $repository); // create savings account - $savingBalance = strval($request->get('savings_balance')) === '' ? '0' : strval($request->get('savings_balance')); - if (bccomp($savingBalance, '0') !== 0) { - $this->createSavingsAccount($request, $repository); - $count++; - } + $this->createSavingsAccount($request, $repository); - - // create credit card. - $limit = strval($request->get('credit_card_limit')) === '' ? '0' : strval($request->get('credit_card_limit')); - if (bccomp($limit, '0') !== 0) { - $this->storeCreditCard($request, $repository); - $count++; - } - $message = strval(trans('firefly.stored_new_accounts_new_user')); - if ($count === 1) { - $message = strval(trans('firefly.stored_new_account_new_user')); - } - - Session::flash('success', $message); + Session::flash('success', strval(trans('firefly.stored_new_accounts_new_user'))); Preferences::mark(); return redirect(route('index')); @@ -152,29 +134,4 @@ class NewUserController extends Controller return true; } - /** - * @param NewUserFormRequest $request - * @param AccountRepositoryInterface $repository - * - * @return bool - */ - private function storeCreditCard(NewUserFormRequest $request, AccountRepositoryInterface $repository): bool - { - $creditAccount = [ - 'name' => 'Credit card', - 'iban' => null, - 'accountType' => 'asset', - 'virtualBalance' => round($request->get('credit_card_limit'), 12), - 'active' => true, - 'accountRole' => 'ccAsset', - 'openingBalance' => null, - 'openingBalanceDate' => null, - 'openingBalanceCurrency' => intval($request->input('amount_currency_id_credit_card_limit')), - 'ccType' => 'monthlyFull', - 'ccMonthlyPaymentDate' => Carbon::now()->year . '-01-01', - ]; - $repository->store($creditAccount); - - return true; - } } diff --git a/app/Support/Amount.php b/app/Support/Amount.php index 6d68b4b455..071c3e48b6 100644 --- a/app/Support/Amount.php +++ b/app/Support/Amount.php @@ -311,6 +311,9 @@ class Amount $coloured = false; $format = '%s'; } + if($transaction->transaction_type_type === TransactionType::OPENING_BALANCE) { + $amount = strval($transaction->transaction_amount); + } $currency = new TransactionCurrency; $currency->symbol = $transaction->transaction_currency_symbol; diff --git a/public/css/bootstrap-tour.min.css b/public/css/bootstrap-tour.min.css deleted file mode 100755 index 8ebd3d3750..0000000000 --- a/public/css/bootstrap-tour.min.css +++ /dev/null @@ -1,22 +0,0 @@ -/* ======================================================================== - * bootstrap-tour - v0.10.3 - * http://bootstraptour.com - * ======================================================================== - * Copyright 2012-2015 Ulrich Sossou - * - * ======================================================================== - * Licensed under the MIT License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ======================================================================== - */ - -.tour-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1100;background-color:#000;opacity:.8;filter:alpha(opacity=80)}.tour-step-backdrop{position:relative;z-index:1101}.tour-step-backdrop>td{position:relative;z-index:1101}.tour-step-background{position:absolute!important;z-index:1100;background:inherit;border-radius:6px}.popover[class*=tour-]{z-index:1102}.popover[class*=tour-] .popover-navigation{padding:9px 14px;overflow:hidden}.popover[class*=tour-] .popover-navigation [data-role=end]{float:right}.popover[class*=tour-] .popover-navigation [data-role=prev],.popover[class*=tour-] .popover-navigation [data-role=next],.popover[class*=tour-] .popover-navigation [data-role=end]{cursor:pointer}.popover[class*=tour-] .popover-navigation [data-role=prev].disabled,.popover[class*=tour-] .popover-navigation [data-role=next].disabled,.popover[class*=tour-] .popover-navigation [data-role=end].disabled{cursor:default}.popover[class*=tour-].orphan{position:fixed;margin-top:0}.popover[class*=tour-].orphan .arrow{display:none} \ No newline at end of file diff --git a/public/js/ff/index.js b/public/js/ff/index.js index 242768712d..58005bac67 100644 --- a/public/js/ff/index.js +++ b/public/js/ff/index.js @@ -8,36 +8,15 @@ * See the LICENSE file for details. */ -/** global: Tour, showTour, accountFrontpageUri, token, billCount, accountExpenseUri, accountRevenueUri */ +/** global: accountFrontpageUri, token, billCount, accountExpenseUri, accountRevenueUri */ $(function () { "use strict"; // do chart JS stuff. drawChart(); - if (showTour === true) { - $.getJSON('json/tour').done(function (data) { - var tour = new Tour( - { - steps: data.steps, - template: data.template, - onEnd: endTheTour - }); - // Initialize the tour - tour.init(); - // Start the tour - tour.start(); - }); - } - }); -function endTheTour() { - "use strict"; - $.post('json/end-tour', {_token: token}); - -} - function drawChart() { "use strict"; lineChart(accountFrontpageUri, 'accounts-chart'); diff --git a/public/js/ff/intro/intro.js b/public/js/ff/intro/intro.js new file mode 100644 index 0000000000..42a2ef7cb6 --- /dev/null +++ b/public/js/ff/intro/intro.js @@ -0,0 +1,14 @@ +/* + * intro.js + * Copyright (c) 2017 thegrumpydictator@gmail.com + * This software may be modified and distributed under the terms of the Creative Commons Attribution-ShareAlike 4.0 International License. + * + * See the LICENSE file for details. + */ + +/** global: route_for_tour */ + +$(function () { + "use strict"; + alert('show user intro for ' + route_for_tour); +}); \ No newline at end of file diff --git a/public/js/lib/bootstrap-tour.min.js b/public/js/lib/bootstrap-tour.min.js deleted file mode 100755 index a481cecfb4..0000000000 --- a/public/js/lib/bootstrap-tour.min.js +++ /dev/null @@ -1,22 +0,0 @@ -/* ======================================================================== - * bootstrap-tour - v0.10.3 - * http://bootstraptour.com - * ======================================================================== - * Copyright 2012-2015 Ulrich Sossou - * - * ======================================================================== - * Licensed under the MIT License (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://opensource.org/licenses/MIT - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * ======================================================================== - */ - -!function(t,e){return"function"==typeof define&&define.amd?define(["jquery"],function(o){return t.Tour=e(o)}):"object"==typeof exports?module.exports=e(require("jQuery")):t.Tour=e(t.jQuery)}(window,function(t){var e,o;return o=window.document,e=function(){function e(e){var o;try{o=window.localStorage}catch(n){o=!1}this._options=t.extend({name:"tour",steps:[],container:"body",autoscroll:!0,keyboard:!0,storage:o,debug:!1,backdrop:!1,backdropContainer:"body",backdropPadding:0,redirect:!0,orphan:!1,duration:!1,delay:!1,basePath:"",template:'',afterSetState:function(){},afterGetState:function(){},afterRemoveState:function(){},onStart:function(){},onEnd:function(){},onShow:function(){},onShown:function(){},onHide:function(){},onHidden:function(){},onNext:function(){},onPrev:function(){},onPause:function(){},onResume:function(){},onRedirectError:function(){}},e),this._force=!1,this._inited=!1,this._current=null,this.backdrop={overlay:null,$element:null,$background:null,backgroundShown:!1,overlayElementShown:!1}}return e.prototype.addSteps=function(t){var e,o,n;for(o=0,n=t.length;n>o;o++)e=t[o],this.addStep(e);return this},e.prototype.addStep=function(t){return this._options.steps.push(t),this},e.prototype.getStep=function(e){return null!=this._options.steps[e]?t.extend({id:"step-"+e,path:"",host:"",placement:"right",title:"",content:"

",next:e===this._options.steps.length-1?-1:e+1,prev:e-1,animation:!0,container:this._options.container,autoscroll:this._options.autoscroll,backdrop:this._options.backdrop,backdropContainer:this._options.backdropContainer,backdropPadding:this._options.backdropPadding,redirect:this._options.redirect,reflexElement:this._options.steps[e].element,backdropElement:this._options.steps[e].element,orphan:this._options.orphan,duration:this._options.duration,delay:this._options.delay,template:this._options.template,onShow:this._options.onShow,onShown:this._options.onShown,onHide:this._options.onHide,onHidden:this._options.onHidden,onNext:this._options.onNext,onPrev:this._options.onPrev,onPause:this._options.onPause,onResume:this._options.onResume,onRedirectError:this._options.onRedirectError},this._options.steps[e]):void 0},e.prototype.init=function(t){return this._force=t,this.ended()?(this._debug("Tour ended, init prevented."),this):(this.setCurrentStep(),this._initMouseNavigation(),this._initKeyboardNavigation(),this._onResize(function(t){return function(){return t.showStep(t._current)}}(this)),null!==this._current&&this.showStep(this._current),this._inited=!0,this)},e.prototype.start=function(t){var e;return null==t&&(t=!1),this._inited||this.init(t),null===this._current&&(e=this._makePromise(null!=this._options.onStart?this._options.onStart(this):void 0),this._callOnPromiseDone(e,this.showStep,0)),this},e.prototype.next=function(){var t;return t=this.hideStep(this._current,this._current+1),this._callOnPromiseDone(t,this._showNextStep)},e.prototype.prev=function(){var t;return t=this.hideStep(this._current,this._current-1),this._callOnPromiseDone(t,this._showPrevStep)},e.prototype.goTo=function(t){var e;return e=this.hideStep(this._current,t),this._callOnPromiseDone(e,this.showStep,t)},e.prototype.end=function(){var e,n;return e=function(e){return function(){return t(o).off("click.tour-"+e._options.name),t(o).off("keyup.tour-"+e._options.name),t(window).off("resize.tour-"+e._options.name),e._setState("end","yes"),e._inited=!1,e._force=!1,e._clearTimer(),null!=e._options.onEnd?e._options.onEnd(e):void 0}}(this),n=this.hideStep(this._current),this._callOnPromiseDone(n,e)},e.prototype.ended=function(){return!this._force&&!!this._getState("end")},e.prototype.restart=function(){return this._removeState("current_step"),this._removeState("end"),this._removeState("redirect_to"),this.start()},e.prototype.pause=function(){var t;return t=this.getStep(this._current),t&&t.duration?(this._paused=!0,this._duration-=(new Date).getTime()-this._start,window.clearTimeout(this._timer),this._debug("Paused/Stopped step "+(this._current+1)+" timer ("+this._duration+" remaining)."),null!=t.onPause?t.onPause(this,this._duration):void 0):this},e.prototype.resume=function(){var t;return t=this.getStep(this._current),t&&t.duration?(this._paused=!1,this._start=(new Date).getTime(),this._duration=this._duration||t.duration,this._timer=window.setTimeout(function(t){return function(){return t._isLast()?t.next():t.end()}}(this),this._duration),this._debug("Started step "+(this._current+1)+" timer with duration "+this._duration),null!=t.onResume&&this._duration!==t.duration?t.onResume(this,this._duration):void 0):this},e.prototype.hideStep=function(e,o){var n,r,i,s;return(s=this.getStep(e))?(this._clearTimer(),i=this._makePromise(null!=s.onHide?s.onHide(this,e):void 0),r=function(n){return function(){var r,i;return r=t(s.element),r.data("bs.popover")||r.data("popover")||(r=t("body")),r.popover("destroy").removeClass("tour-"+n._options.name+"-element tour-"+n._options.name+"-"+e+"-element").removeData("bs.popover").focus(),s.reflex&&t(s.reflexElement).removeClass("tour-step-element-reflex").off(""+n._reflexEvent(s.reflex)+".tour-"+n._options.name),s.backdrop&&(i=null!=o&&n.getStep(o),i&&i.backdrop&&i.backdropElement===s.backdropElement||n._hideBackdrop()),null!=s.onHidden?s.onHidden(n):void 0}}(this),n=s.delay.hide||s.delay,"[object Number]"==={}.toString.call(n)&&n>0?(this._debug("Wait "+n+" milliseconds to hide the step "+(this._current+1)),window.setTimeout(function(t){return function(){return t._callOnPromiseDone(i,r)}}(this),n)):this._callOnPromiseDone(i,r),i):void 0},e.prototype.showStep=function(t){var e,n,r,i,s,a;return this.ended()?(this._debug("Tour ended, showStep prevented."),this):(a=this.getStep(t),a&&(s=t0?(this._debug("Wait "+r+" milliseconds to show the step "+(this._current+1)),window.setTimeout(function(t){return function(){return t._callOnPromiseDone(n,i)}}(this),r)):this._callOnPromiseDone(n,i),n):void 0)},e.prototype.getCurrentStep=function(){return this._current},e.prototype.setCurrentStep=function(t){return null!=t?(this._current=t,this._setState("current_step",t)):(this._current=this._getState("current_step"),this._current=null===this._current?null:parseInt(this._current,10)),this},e.prototype.redraw=function(){return this._showOverlayElement(this.getStep(this.getCurrentStep()).element,!0)},e.prototype._setState=function(t,e){var o,n;if(this._options.storage){n=""+this._options.name+"_"+t;try{this._options.storage.setItem(n,e)}catch(r){o=r,o.code===DOMException.QUOTA_EXCEEDED_ERR&&this._debug("LocalStorage quota exceeded. State storage failed.")}return this._options.afterSetState(n,e)}return null==this._state&&(this._state={}),this._state[t]=e},e.prototype._removeState=function(t){var e;return this._options.storage?(e=""+this._options.name+"_"+t,this._options.storage.removeItem(e),this._options.afterRemoveState(e)):null!=this._state?delete this._state[t]:void 0},e.prototype._getState=function(t){var e,o;return this._options.storage?(e=""+this._options.name+"_"+t,o=this._options.storage.getItem(e)):null!=this._state&&(o=this._state[t]),(void 0===o||"null"===o)&&(o=null),this._options.afterGetState(t,o),o},e.prototype._showNextStep=function(){var t,e,o;return o=this.getStep(this._current),e=function(t){return function(){return t.showStep(o.next)}}(this),t=this._makePromise(null!=o.onNext?o.onNext(this):void 0),this._callOnPromiseDone(t,e)},e.prototype._showPrevStep=function(){var t,e,o;return o=this.getStep(this._current),e=function(t){return function(){return t.showStep(o.prev)}}(this),t=this._makePromise(null!=o.onPrev?o.onPrev(this):void 0),this._callOnPromiseDone(t,e)},e.prototype._debug=function(t){return this._options.debug?window.console.log("Bootstrap Tour '"+this._options.name+"' | "+t):void 0},e.prototype._isRedirect=function(t,e,o){var n;return null!=t&&""!==t&&("[object RegExp]"==={}.toString.call(t)&&!t.test(o.origin)||"[object String]"==={}.toString.call(t)&&this._isHostDifferent(t,o))?!0:(n=[o.pathname,o.search,o.hash].join(""),null!=e&&""!==e&&("[object RegExp]"==={}.toString.call(e)&&!e.test(n)||"[object String]"==={}.toString.call(e)&&this._isPathDifferent(e,n)))},e.prototype._isHostDifferent=function(t,e){switch({}.toString.call(t)){case"[object RegExp]":return!t.test(e.origin);case"[object String]":return this._getProtocol(t)!==this._getProtocol(e.href)||this._getHost(t)!==this._getHost(e.href);default:return!0}},e.prototype._isPathDifferent=function(t,e){return this._getPath(t)!==this._getPath(e)||!this._equal(this._getQuery(t),this._getQuery(e))||!this._equal(this._getHash(t),this._getHash(e))},e.prototype._isJustPathHashDifferent=function(t,e,o){var n;return null!=t&&""!==t&&this._isHostDifferent(t,o)?!1:(n=[o.pathname,o.search,o.hash].join(""),"[object String]"==={}.toString.call(e)?this._getPath(e)===this._getPath(n)&&this._equal(this._getQuery(e),this._getQuery(n))&&!this._equal(this._getHash(e),this._getHash(n)):!1)},e.prototype._redirect=function(e,n,r){var i;return t.isFunction(e.redirect)?e.redirect.call(this,r):(i="[object String]"==={}.toString.call(e.host)?""+e.host+r:r,this._debug("Redirect to "+i),this._getState("redirect_to")!==""+n?(this._setState("redirect_to",""+n),o.location.href=i):(this._debug("Error redirection loop to "+r),this._removeState("redirect_to"),null!=e.onRedirectError?e.onRedirectError(this):void 0))},e.prototype._isOrphan=function(e){return null==e.element||!t(e.element).length||t(e.element).is(":hidden")&&"http://www.w3.org/2000/svg"!==t(e.element)[0].namespaceURI},e.prototype._isLast=function(){return this._current").parent().html()},e.prototype._reflexEvent=function(t){return"[object Boolean]"==={}.toString.call(t)?"click":t},e.prototype._focus=function(t,e,o){var n,r;return r=o?"end":"next",n=t.find("[data-role='"+r+"']"),e.on("shown.bs.popover",function(){return n.focus()})},e.prototype._reposition=function(e,n){var r,i,s,a,u,p,h;if(a=e[0].offsetWidth,i=e[0].offsetHeight,h=e.offset(),u=h.left,p=h.top,r=t(o).outerHeight()-h.top-e.outerHeight(),0>r&&(h.top=h.top+r),s=t("html").outerWidth()-h.left-e.outerWidth(),0>s&&(h.left=h.left+s),h.top<0&&(h.top=0),h.left<0&&(h.left=0),e.offset(h),"bottom"===n.placement||"top"===n.placement){if(u!==h.left)return this._replaceArrow(e,2*(h.left-u),a,"left")}else if(p!==h.top)return this._replaceArrow(e,2*(h.top-p),i,"top")},e.prototype._center=function(e){return e.css("top",t(window).outerHeight()/2-e.outerHeight()/2)},e.prototype._replaceArrow=function(t,e,o,n){return t.find(".arrow").css(n,e?50*(1-e/o)+"%":"")},e.prototype._scrollIntoView=function(e,o){var n,r,i,s,a,u,p;if(n=t(e.element),!n.length)return o();switch(r=t(window),a=n.offset().top,s=n.outerHeight(),p=r.height(),u=0,e.placement){case"top":u=Math.max(0,a-p/2);break;case"left":case"right":u=Math.max(0,a+s/2-p/2);break;case"bottom":u=Math.max(0,a+s-p/2)}return this._debug("Scroll into view. ScrollTop: "+u+". Element offset: "+a+". Window height: "+p+"."),i=0,t("body, html").stop(!0,!0).animate({scrollTop:Math.ceil(u)},function(t){return function(){return 2===++i?(o(),t._debug("Scroll into view.\nAnimation end element offset: "+n.offset().top+".\nWindow height: "+r.height()+".")):void 0}}(this))},e.prototype._onResize=function(e,o){return t(window).on("resize.tour-"+this._options.name,function(){return clearTimeout(o),o=setTimeout(e,100)})},e.prototype._initMouseNavigation=function(){var e;return e=this,t(o).off("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='prev']").off("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='next']").off("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='end']").off("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='pause-resume']").on("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='next']",function(t){return function(e){return e.preventDefault(),t.next()}}(this)).on("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='prev']",function(t){return function(e){return e.preventDefault(),t._current>0?t.prev():void 0}}(this)).on("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='end']",function(t){return function(e){return e.preventDefault(),t.end()}}(this)).on("click.tour-"+this._options.name,".popover.tour-"+this._options.name+" *[data-role='pause-resume']",function(o){var n;return o.preventDefault(),n=t(this),n.text(e._paused?n.data("pause-text"):n.data("resume-text")),e._paused?e.resume():e.pause()})},e.prototype._initKeyboardNavigation=function(){return this._options.keyboard?t(o).on("keyup.tour-"+this._options.name,function(t){return function(e){if(e.which)switch(e.which){case 39:return e.preventDefault(),t._isLast()?t.next():t.end();case 37:if(e.preventDefault(),t._current>0)return t.prev()}}}(this)):void 0},e.prototype._makePromise=function(e){return e&&t.isFunction(e.then)?e:null},e.prototype._callOnPromiseDone=function(t,e,o){return t?t.then(function(t){return function(){return e.call(t,o)}}(this)):e.call(this,o)},e.prototype._showBackdrop=function(e){return this.backdrop.backgroundShown?void 0:(this.backdrop=t("
",{"class":"tour-backdrop"}),this.backdrop.backgroundShown=!0,t(e.backdropContainer).append(this.backdrop))},e.prototype._hideBackdrop=function(){return this._hideOverlayElement(),this._hideBackground()},e.prototype._hideBackground=function(){return this.backdrop&&this.backdrop.remove?(this.backdrop.remove(),this.backdrop.overlay=null,this.backdrop.backgroundShown=!1):void 0},e.prototype._showOverlayElement=function(e,o){var n,r,i;return r=t(e.element),n=t(e.backdropElement),!r||0===r.length||this.backdrop.overlayElementShown&&!o?void 0:(this.backdrop.overlayElementShown||(this.backdrop.$element=n.addClass("tour-step-backdrop"),this.backdrop.$background=t("
",{"class":"tour-step-background"}),this.backdrop.$background.appendTo(e.backdropContainer),this.backdrop.overlayElementShown=!0),i={width:n.innerWidth(),height:n.innerHeight(),offset:n.offset()},e.backdropPadding&&(i=this._applyBackdropPadding(e.backdropPadding,i)),this.backdrop.$background.width(i.width).height(i.height).offset(i.offset))},e.prototype._hideOverlayElement=function(){return this.backdrop.overlayElementShown?(this.backdrop.$element.removeClass("tour-step-backdrop"),this.backdrop.$background.remove(),this.backdrop.$element=null,this.backdrop.$background=null,this.backdrop.overlayElementShown=!1):void 0},e.prototype._applyBackdropPadding=function(t,e){return"object"==typeof t?(null==t.top&&(t.top=0),null==t.right&&(t.right=0),null==t.bottom&&(t.bottom=0),null==t.left&&(t.left=0),e.offset.top=e.offset.top-t.top,e.offset.left=e.offset.left-t.left,e.width=e.width+t.left+t.right,e.height=e.height+t.top+t.bottom):(e.offset.top=e.offset.top-t,e.offset.left=e.offset.left-t,e.width=e.width+2*t,e.height=e.height+2*t),e},e.prototype._clearTimer=function(){return window.clearTimeout(this._timer),this._timer=null,this._duration=null},e.prototype._getProtocol=function(t){return t=t.split("://"),t.length>1?t[0]:"http"},e.prototype._getHost=function(t){return t=t.split("//"),t=t.length>1?t[1]:t[0],t.split("/")[0]},e.prototype._getPath=function(t){return t.replace(/\/?$/,"").split("?")[0].split("#")[0]},e.prototype._getQuery=function(t){return this._getParams(t,"?")},e.prototype._getHash=function(t){return this._getParams(t,"#")},e.prototype._getParams=function(t,e){var o,n,r,i,s;if(n=t.split(e),1===n.length)return{};for(n=n[1].split("&"),r={},i=0,s=n.length;s>i;i++)o=n[i],o=o.split("="),r[o[0]]=o[1]||"";return r},e.prototype._equal=function(t,e){var o,n,r,i,s,a;if("[object Object]"==={}.toString.call(t)&&"[object Object]"==={}.toString.call(e)){if(n=Object.keys(t),r=Object.keys(e),n.length!==r.length)return!1;for(o in t)if(i=t[o],!this._equal(e[o],i))return!1;return!0}if("[object Array]"==={}.toString.call(t)&&"[object Array]"==={}.toString.call(e)){if(t.length!==e.length)return!1;for(o=s=0,a=t.length;a>s;o=++s)if(i=t[o],!this._equal(i,e[o]))return!1;return!0}return t===e},e}()}); \ No newline at end of file diff --git a/public/lib/intro/intro.min.js b/public/lib/intro/intro.min.js new file mode 100755 index 0000000000..875a703104 --- /dev/null +++ b/public/lib/intro/intro.min.js @@ -0,0 +1,49 @@ +(function(C,n){"object"===typeof exports?n(exports):"function"===typeof define&&define.amd?define(["exports"],n):n(C)})(this,function(C){function n(a){this._targetElement=a;this._introItems=[];this._options={nextLabel:"Next →",prevLabel:"← Back",skipLabel:"Skip",doneLabel:"Done",hidePrev:!1,hideNext:!1,tooltipPosition:"bottom",tooltipClass:"",highlightClass:"",exitOnEsc:!0,exitOnOverlayClick:!0,showStepNumbers:!0,keyboardNavigation:!0,showButtons:!0,showBullets:!0,showProgress:!1,scrollToElement:!0, +overlayOpacity:0.8,scrollPadding:30,positionPrecedence:["bottom","top","right","left"],disableInteraction:!1,hintPosition:"top-middle",hintButtonLabel:"Got it",hintAnimation:!0}}function V(a){var b=[],c=this;if(this._options.steps)for(var d=0,e=this._options.steps.length;de.length)return!1;d=0;for(f=e.length;dk.width||0>h.left+h.width/2-m?(s(g,"bottom"),s(g,"top")):(h.height+h.top+w>k.height&&s(g,"bottom"),0>h.top-w&&s(g,"top"));h.width+h.left+m>k.width&&s(g,"right");0>h.left-m&&s(g,"left");0g.height?(c.className="introjs-arrow left-bottom",b.style.top="-"+(a.height-f.height-20)+"px"):c.className="introjs-arrow left";break;case "left":e||!0!=this._options.showStepNumbers||(b.style.top="15px");f.top+a.height>g.height?(b.style.top="-"+(a.height-f.height-20)+"px", +c.className="introjs-arrow right-bottom"):c.className="introjs-arrow right";b.style.right=f.width+20+"px";break;case "floating":c.style.display="none";b.style.left="50%";b.style.top="50%";b.style.marginLeft="-"+a.width/2+"px";b.style.marginTop="-"+a.height/2+"px";"undefined"!=typeof d&&null!=d&&(d.style.left="-"+(a.width/2+18)+"px",d.style.top="-"+(a.height/2+18)+"px");break;case "bottom-right-aligned":c.className="introjs-arrow top-right";P(f,0,a,b);b.style.top=f.height+20+"px";break;case "bottom-middle-aligned":c.className= +"introjs-arrow top-middle";c=f.width/2-a.width/2;e&&(c+=5);P(f,c,a,b)&&(b.style.right=null,H(f,c,a,g,b));b.style.top=f.height+20+"px";break;default:c.className="introjs-arrow top",H(f,0,a,g,b),b.style.top=f.height+20+"px"}}}function H(a,b,c,d,e){if(a.left+b+c.width>d.width)return e.style.left=d.width-c.width-a.left+"px",!1;e.style.left=b+"px";return!0}function P(a,b,c,d){if(0>a.left+a.width-b-c.width)return d.style.left=-a.left+"px",!1;d.style.right=b+"px";return!0}function s(a,b){-1 a.active").className="",d.querySelector('.introjs-bullets li > a[data-stepnumber="'+a.step+'"]').className="active");d.querySelector(".introjs-progress .introjs-progressbar").setAttribute("style","width:"+Q.call(b)+"%;");w.style.opacity=1;f&&(f.style.opacity=1);-1===l.tabIndex?m.focus():l.focus()},350)}else{var n=document.createElement("div"),h=document.createElement("div"),c=document.createElement("div"),q=document.createElement("div"),r=document.createElement("div"), +s=document.createElement("div"),v=document.createElement("div"),A=document.createElement("div");n.className=e;h.className="introjs-tooltipReferenceLayer";t.call(b,n);t.call(b,h);this._targetElement.appendChild(n);this._targetElement.appendChild(h);c.className="introjs-arrow";r.className="introjs-tooltiptext";r.innerHTML=a.intro;s.className="introjs-bullets";!1===this._options.showBullets&&(s.style.display="none");for(var n=document.createElement("ul"),e=0,C=this._introItems.length;ec||a.element.clientHeight>p?window.scrollBy(0,c-this._options.scrollPadding):window.scrollBy(0,q+70+this._options.scrollPadding));"undefined"!==typeof this._introAfterChangeCallback&&this._introAfterChangeCallback.call(this,a.element)}function O(){for(var a=document.querySelectorAll(".introjs-showElement"), +b=0,c=a.length;bc||"none"!==d&&void 0!==d)b.className+=" introjs-fixParent";b=b.parentNode}}function J(a, +b){if(a instanceof SVGElement){var c=a.getAttribute("class")||"";a.setAttribute("class",c+" "+b)}else a.className+=" "+b}function r(a,b){var c="";a.currentStyle?c=a.currentStyle[b]:document.defaultView&&document.defaultView.getComputedStyle&&(c=document.defaultView.getComputedStyle(a,null).getPropertyValue(b));return c&&c.toLowerCase?c.toLowerCase():c}function I(a){var b=a.parentNode;return b&&"HTML"!==b.nodeName?"fixed"==r(a,"position")?!0:I(b):!1}function G(){if(void 0!=window.innerWidth)return{width:window.innerWidth, +height:window.innerHeight};var a=document.documentElement;return{width:a.clientWidth,height:a.clientHeight}}function Z(a){a=a.getBoundingClientRect();return 0<=a.top&&0<=a.left&&a.bottom+80<=window.innerHeight&&a.right<=window.innerWidth}function W(a){var b=document.createElement("div"),c="",d=this;b.className="introjs-overlay";if(a.tagName&&"body"!==a.tagName.toLowerCase()){var e=u(a);e&&(c+="width: "+e.width+"px; height:"+e.height+"px; top:"+e.top+"px;left: "+e.left+"px;",b.setAttribute("style", +c))}else c+="top: 0;bottom: 0; left: 0;right: 0;position: fixed;",b.setAttribute("style",c);a.appendChild(b);b.onclick=function(){!0==d._options.exitOnOverlayClick&&z.call(d,a)};setTimeout(function(){c+="opacity: "+d._options.overlayOpacity.toString()+";";b.setAttribute("style",c)},10);return!0}function v(){var a=this._targetElement.querySelector(".introjs-hintReference");if(a){var b=a.getAttribute("data-step");a.parentNode.removeChild(a);return b}}function R(a){this._introItems=[];if(this._options.hints){a= +0;for(var b=this._options.hints.length;ac.length)return!1;a=0;for(b=c.length;atd,tr.introjs-showElement>th{z-index:9999999!important}.introjs-disableInteraction{z-index:99999999!important;position:absolute;background-color:white;opacity:0;filter:alpha(opacity=0)}.introjs-relativePosition,tr.introjs-showElement>td,tr.introjs-showElement>th{position:relative}.introjs-helperLayer{box-sizing:content-box;position:absolute;z-index:9999998;background-color:#FFF;background-color:rgba(255,255,255,.9);border:1px solid #777;border:1px solid rgba(0,0,0,.5);border-radius:4px;box-shadow:0 2px 15px rgba(0,0,0,.4);-webkit-transition:all .3s ease-out;-moz-transition:all .3s ease-out;-ms-transition:all .3s ease-out;-o-transition:all .3s ease-out;transition:all .3s ease-out}.introjs-tooltipReferenceLayer{box-sizing:content-box;position:absolute;visibility:hidden;z-index:10000000;background-color:transparent;-webkit-transition:all .3s ease-out;-moz-transition:all .3s ease-out;-ms-transition:all .3s ease-out;-o-transition:all .3s ease-out;transition:all .3s ease-out}.introjs-helperLayer *,.introjs-helperLayer *:before,.introjs-helperLayer *:after{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;-ms-box-sizing:content-box;-o-box-sizing:content-box;box-sizing:content-box}.introjs-helperNumberLayer{box-sizing:content-box;position:absolute;visibility:visible;top:-16px;left:-16px;z-index:9999999999!important;padding:2px;font-family:Arial,verdana,tahoma;font-size:13px;font-weight:bold;color:white;text-align:center;text-shadow:1px 1px 1px rgba(0,0,0,.3);background:#ff3019;background:-webkit-linear-gradient(top,#ff3019 0,#cf0404 100%);background:-webkit-gradient(linear,left top,left bottom,color-stop(0%,#ff3019),color-stop(100%,#cf0404));background:-moz-linear-gradient(top,#ff3019 0,#cf0404 100%);background:-ms-linear-gradient(top,#ff3019 0,#cf0404 100%);background:-o-linear-gradient(top,#ff3019 0,#cf0404 100%);background:linear-gradient(to bottom,#ff3019 0,#cf0404 100%);width:20px;height:20px;line-height:20px;border:3px solid white;border-radius:50%;filter:"progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3019', endColorstr='#cf0404', GradientType=0)";filter:"progid:DXImageTransform.Microsoft.Shadow(direction=135, strength=2, color=ff0000)";box-shadow:0 2px 5px rgba(0,0,0,.4)}.introjs-arrow{border:5px solid white;content:'';position:absolute}.introjs-arrow.top{top:-10px;border-top-color:transparent;border-right-color:transparent;border-bottom-color:white;border-left-color:transparent}.introjs-arrow.top-right{top:-10px;right:10px;border-top-color:transparent;border-right-color:transparent;border-bottom-color:white;border-left-color:transparent}.introjs-arrow.top-middle{top:-10px;left:50%;margin-left:-5px;border-top-color:transparent;border-right-color:transparent;border-bottom-color:white;border-left-color:transparent}.introjs-arrow.right{right:-10px;top:10px;border-top-color:transparent;border-right-color:transparent;border-bottom-color:transparent;border-left-color:white}.introjs-arrow.right-bottom{bottom:10px;right:-10px;border-top-color:transparent;border-right-color:transparent;border-bottom-color:transparent;border-left-color:white}.introjs-arrow.bottom{bottom:-10px;border-top-color:white;border-right-color:transparent;border-bottom-color:transparent;border-left-color:transparent}.introjs-arrow.left{left:-10px;top:10px;border-top-color:transparent;border-right-color:white;border-bottom-color:transparent;border-left-color:transparent}.introjs-arrow.left-bottom{left:-10px;bottom:10px;border-top-color:transparent;border-right-color:white;border-bottom-color:transparent;border-left-color:transparent}.introjs-tooltip{box-sizing:content-box;position:absolute;visibility:visible;padding:10px;background-color:white;min-width:200px;max-width:300px;border-radius:3px;box-shadow:0 1px 10px rgba(0,0,0,.4);-webkit-transition:opacity .1s ease-out;-moz-transition:opacity .1s ease-out;-ms-transition:opacity .1s ease-out;-o-transition:opacity .1s ease-out;transition:opacity .1s ease-out}.introjs-tooltipbuttons{text-align:right;white-space:nowrap}.introjs-button{box-sizing:content-box;position:relative;overflow:visible;display:inline-block;padding:.3em .8em;border:1px solid #d4d4d4;margin:0;text-decoration:none;text-shadow:1px 1px 0 #fff;font:11px/normal sans-serif;color:#333;white-space:nowrap;cursor:pointer;outline:0;background-color:#ececec;background-image:-webkit-gradient(linear,0 0,0 100%,from(#f4f4f4),to(#ececec));background-image:-moz-linear-gradient(#f4f4f4,#ececec);background-image:-o-linear-gradient(#f4f4f4,#ececec);background-image:linear-gradient(#f4f4f4,#ececec);-webkit-background-clip:padding;-moz-background-clip:padding;-o-background-clip:padding-box;-webkit-border-radius:.2em;-moz-border-radius:.2em;border-radius:.2em;zoom:1;*display:inline;margin-top:10px}.introjs-button:hover{border-color:#bcbcbc;text-decoration:none;box-shadow:0 1px 1px #e3e3e3}.introjs-button:focus,.introjs-button:active{background-image:-webkit-gradient(linear,0 0,0 100%,from(#ececec),to(#f4f4f4));background-image:-moz-linear-gradient(#ececec,#f4f4f4);background-image:-o-linear-gradient(#ececec,#f4f4f4);background-image:linear-gradient(#ececec,#f4f4f4)}.introjs-button::-moz-focus-inner{padding:0;border:0}.introjs-skipbutton{box-sizing:content-box;margin-right:5px;color:#7a7a7a}.introjs-prevbutton{-webkit-border-radius:.2em 0 0 .2em;-moz-border-radius:.2em 0 0 .2em;border-radius:.2em 0 0 .2em;border-right:0}.introjs-prevbutton.introjs-fullbutton{border:1px solid #d4d4d4;-webkit-border-radius:.2em;-moz-border-radius:.2em;border-radius:.2em}.introjs-nextbutton{-webkit-border-radius:0 .2em .2em 0;-moz-border-radius:0 .2em .2em 0;border-radius:0 .2em .2em 0}.introjs-nextbutton.introjs-fullbutton{-webkit-border-radius:.2em;-moz-border-radius:.2em;border-radius:.2em}.introjs-disabled,.introjs-disabled:hover,.introjs-disabled:focus{color:#9a9a9a;border-color:#d4d4d4;box-shadow:none;cursor:default;background-color:#f4f4f4;background-image:none;text-decoration:none}.introjs-hidden{display:none}.introjs-bullets{text-align:center}.introjs-bullets ul{box-sizing:content-box;clear:both;margin:15px auto 0;padding:0;display:inline-block}.introjs-bullets ul li{box-sizing:content-box;list-style:none;float:left;margin:0 2px}.introjs-bullets ul li a{box-sizing:content-box;display:block;width:6px;height:6px;background:#ccc;border-radius:10px;-moz-border-radius:10px;-webkit-border-radius:10px;text-decoration:none;cursor:pointer}.introjs-bullets ul li a:hover{background:#999}.introjs-bullets ul li a.active{background:#999}.introjs-progress{box-sizing:content-box;overflow:hidden;height:10px;margin:10px 0 5px 0;border-radius:4px;background-color:#ecf0f1}.introjs-progressbar{box-sizing:content-box;float:left;width:0;height:100%;font-size:10px;line-height:10px;text-align:center;background-color:#08c}.introjsFloatingElement{position:absolute;height:0;width:0;left:50%;top:50%}.introjs-fixedTooltip{position:fixed}.introjs-hint{box-sizing:content-box;position:absolute;background:transparent;width:20px;height:15px;cursor:pointer}.introjs-hint:focus{border:0;outline:0}.introjs-hidehint{display:none}.introjs-fixedhint{position:fixed}.introjs-hint:hover>.introjs-hint-pulse{border:5px solid rgba(60,60,60,0.57)}.introjs-hint-pulse{box-sizing:content-box;width:10px;height:10px;border:5px solid rgba(60,60,60,0.27);-webkit-border-radius:30px;-moz-border-radius:30px;border-radius:30px;background-color:rgba(136,136,136,0.24);z-index:10;position:absolute;-webkit-transition:all .2s ease-out;-moz-transition:all .2s ease-out;-ms-transition:all .2s ease-out;-o-transition:all .2s ease-out;transition:all .2s ease-out}.introjs-hint-no-anim .introjs-hint-dot{-webkit-animation:none;-moz-animation:none;animation:none}.introjs-hint-dot{box-sizing:content-box;border:10px solid rgba(146,146,146,0.36);background:transparent;-webkit-border-radius:60px;-moz-border-radius:60px;border-radius:60px;height:50px;width:50px;-webkit-animation:introjspulse 3s ease-out;-moz-animation:introjspulse 3s ease-out;animation:introjspulse 3s ease-out;-webkit-animation-iteration-count:infinite;-moz-animation-iteration-count:infinite;animation-iteration-count:infinite;position:absolute;top:-25px;left:-25px;z-index:1;opacity:0}@-moz-keyframes introjspulse{0%{-moz-transform:scale(0);opacity:.0}25%{-moz-transform:scale(0);opacity:.1}50%{-moz-transform:scale(0.1);opacity:.3}75%{-moz-transform:scale(0.5);opacity:.5}100%{-moz-transform:scale(1);opacity:.0}}@-webkit-keyframes introjspulse{0%{-webkit-transform:scale(0);opacity:.0}25%{-webkit-transform:scale(0);opacity:.1}50%{-webkit-transform:scale(0.1);opacity:.3}75%{-webkit-transform:scale(0.5);opacity:.5}100%{-webkit-transform:scale(1);opacity:.0}} \ No newline at end of file diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index db7c42e92b..95b3bec834 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -427,12 +427,6 @@ return [ 'attachment_updated' => 'Updated attachment ":name"', 'upload_max_file_size' => 'Maximum file size: :size', - // tour: - 'prev' => 'Prev', - 'next' => 'Next', - 'end-tour' => 'End tour', - 'pause' => 'Pause', - // transaction index 'title_expenses' => 'Expenses', 'title_withdrawal' => 'Expenses', @@ -500,15 +494,6 @@ return [ 'make_default_currency' => 'make default', 'default_currency' => 'default', - // new user: - 'submit' => 'Submit', - 'getting_started' => 'Getting started', - 'to_get_started' => 'To get started with Firefly, please enter your current bank\'s name, and the balance of your checking account:', - 'savings_balance_text' => 'If you have a savings account, please enter the current balance of your savings account:', - 'cc_balance_text' => 'If you have a credit card, please enter your credit card\'s limit.', - 'stored_new_account_new_user' => 'Yay! Your new account has been stored.', - 'stored_new_accounts_new_user' => 'Yay! Your new accounts have been stored.', - // forms: 'mandatoryFields' => 'Mandatory fields', 'optionalFields' => 'Optional fields', @@ -644,6 +629,12 @@ return [ // new user: 'welcome' => 'Welcome to Firefly!', + 'submit' => 'Submit', + 'getting_started' => 'Getting started', + 'to_get_started' => 'It is good to see you have successfully installed Firefly III. To get started with this tool please enter your bank\'s name and the balance of your main checking account. Do not worry yet if you have multiple accounts. You can add those later. It\'s just that Firefly III needs something to start with.', + 'savings_balance_text' => 'Firefly III will automatically create a savings account for you. By default, there will be no money in your savings account, but if you tell Firefly III the balance it will be stored as such.', + 'finish_up_new_user' => 'That\'s it! You can continue by pressing Submit. You will be taken to the index of Firefly III.', + 'stored_new_accounts_new_user' => 'Yay! Your new accounts have been stored.', // home page: 'yourAccounts' => 'Your accounts', diff --git a/resources/views/index.twig b/resources/views/index.twig index 6cd8914de2..fe9f935511 100644 --- a/resources/views/index.twig +++ b/resources/views/index.twig @@ -4,10 +4,6 @@ {{ Breadcrumbs.renderIfExists }} {% endblock %} {% block content %} - - - - {% include 'partials.boxes' %}
@@ -31,7 +27,7 @@
- + {# CATEGORIES #}

{{ 'categories'|_ }}

@@ -44,7 +40,7 @@
{% if billCount > 0 %} - + {# BILLS #}

{{ 'bills'|_ }}

@@ -59,13 +55,12 @@
{% endif %} - + {# TRANSACTIONS #} {% for data in transactions %}

{{ data[1].name }}

-
@@ -107,7 +102,7 @@
- + {# EXPENSE ACCOUNTS #}

{{ 'expense_accounts'|_ }}

@@ -117,7 +112,7 @@
- + {# OPTIONAL REVENUE ACCOUNTS #} {% if showDepositsFrontpage %}
@@ -134,14 +129,7 @@ {% endblock %} {% block scripts %} -