diff --git a/app/Http/Controllers/Transaction/SingleController.php b/app/Http/Controllers/Transaction/SingleController.php index da12791431..704a7007d4 100644 --- a/app/Http/Controllers/Transaction/SingleController.php +++ b/app/Http/Controllers/Transaction/SingleController.php @@ -25,6 +25,7 @@ use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; +use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use FireflyIII\Repositories\PiggyBank\PiggyBankRepositoryInterface; use Log; @@ -48,7 +49,8 @@ class SingleController extends Controller /** @var BudgetRepositoryInterface */ private $budgets; - + /** @var CurrencyRepositoryInterface */ + private $currency; /** @var PiggyBankRepositoryInterface */ private $piggyBanks; @@ -71,6 +73,7 @@ class SingleController extends Controller $this->budgets = app(BudgetRepositoryInterface::class); $this->piggyBanks = app(PiggyBankRepositoryInterface::class); $this->attachments = app(AttachmentHelperInterface::class); + $this->currency = app(CurrencyRepositoryInterface::class); View::share('title', trans('firefly.transactions')); View::share('mainTitleIcon', 'fa-repeat'); @@ -231,7 +234,6 @@ class SingleController extends Controller // view related code $subTitle = trans('breadcrumbs.edit_journal', ['description' => $journal->description]); - // journal related code $sourceAccounts = $journal->sourceAccountList(); $destinationAccounts = $journal->destinationAccountList(); @@ -249,6 +251,7 @@ class SingleController extends Controller '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'), @@ -256,8 +259,20 @@ class SingleController extends Controller 'invoice_date' => $journal->dateAsString('invoice_date'), 'interal_reference' => $journal->getMeta('internal_reference'), 'notes' => $journal->getMeta('notes'), + + // exchange rate fields + 'exchanged_amount' => $journal->amountPositive(), + 'exchanged_currency' => $journal->transactionCurrency, ]; + // catch possibly exchanged currencies and what-not. + $originalCurrencyId = intval($journal->getMeta('original_currency_id')); + if ($originalCurrencyId > 0) { + // update some fields in pre-filled. + $preFilled['amount'] = $journal->getMeta('original_amount'); + $preFilled['currency'] = $this->currency->find(intval($journal->getMeta('original_currency_id'))); + } + if ($journal->isWithdrawal() && $destinationAccounts->first()->accountType->type == AccountType::CASH) { $preFilled['destination_account_name'] = ''; } diff --git a/app/Http/Controllers/TransactionController.php b/app/Http/Controllers/TransactionController.php index 5fdfa5fa40..efb0a35bcb 100644 --- a/app/Http/Controllers/TransactionController.php +++ b/app/Http/Controllers/TransactionController.php @@ -17,6 +17,7 @@ use Carbon\Carbon; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Collector\JournalCollectorInterface; use FireflyIII\Models\TransactionJournal; +use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface; use FireflyIII\Repositories\Journal\JournalRepositoryInterface; use FireflyIII\Repositories\Journal\JournalTaskerInterface; use FireflyIII\Support\CacheProperties; @@ -177,12 +178,19 @@ class TransactionController extends Controller return $this->redirectToAccount($journal); } - $events = $tasker->getPiggyBankEvents($journal); - $transactions = $tasker->getTransactionsOverview($journal); - $what = strtolower($journal->transaction_type_type ?? $journal->transactionType->type); - $subTitle = trans('firefly.' . $what) . ' "' . e($journal->description) . '"'; + $events = $tasker->getPiggyBankEvents($journal); + $transactions = $tasker->getTransactionsOverview($journal); + $what = strtolower($journal->transaction_type_type ?? $journal->transactionType->type); + $subTitle = trans('firefly.' . $what) . ' "' . e($journal->description) . '"'; + $originalCurrency = null; - return view('transactions.show', compact('journal', 'events', 'subTitle', 'what', 'transactions')); + if ($journal->hasMeta('original_currency_id')) { + /** @var CurrencyRepositoryInterface $repository */ + $repository = app(CurrencyRepositoryInterface::class); + $originalCurrency = $repository->find(intval($journal->hasMeta('original_currency_id'))); + } + + return view('transactions.show', compact('journal', 'events', 'subTitle', 'what', 'transactions', 'originalCurrency')); } diff --git a/app/Http/Requests/JournalFormRequest.php b/app/Http/Requests/JournalFormRequest.php index 56d64b753f..3110d75559 100644 --- a/app/Http/Requests/JournalFormRequest.php +++ b/app/Http/Requests/JournalFormRequest.php @@ -104,6 +104,9 @@ class JournalFormRequest extends Request 'destination_account_id' => 'numeric|belongsToUser:accounts,id', 'destination_account_name' => 'between:1,255', 'piggy_bank_id' => 'between:1,255', + + // exchange rate data: + 'exchanged_amount' => 'numeric|required|more:0', ]; // some rules get an upgrade depending on the type of data: diff --git a/app/Repositories/Journal/JournalRepository.php b/app/Repositories/Journal/JournalRepository.php index 69a24cc128..8fec36bf6d 100644 --- a/app/Repositories/Journal/JournalRepository.php +++ b/app/Repositories/Journal/JournalRepository.php @@ -181,10 +181,10 @@ class JournalRepository implements JournalRepositoryInterface */ $accountCurrencyId = intval($accounts['source']->getMeta('currency_id')); if ($accountCurrencyId !== $currencyId) { - $currencyId = $accountCurrencyId; - $amount = strval($data['exchanged_amount']); $data['original_amount'] = $data['amount']; $data['original_currency_id'] = $currencyId; + $currencyId = $accountCurrencyId; + $amount = strval($data['exchanged_amount']); } break; default: @@ -261,10 +261,22 @@ class JournalRepository implements JournalRepositoryInterface */ public function update(TransactionJournal $journal, array $data): TransactionJournal { + // update actual journal: - $journal->transaction_currency_id = $data['currency_id']; - $journal->description = $data['description']; - $journal->date = $data['date']; + $journal->description = $data['description']; + $journal->date = $data['date']; + $accounts = $this->storeAccounts($journal->transactionType, $data); + $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(); @@ -272,12 +284,9 @@ class JournalRepository implements JournalRepositoryInterface $this->storeCategoryWithJournal($journal, $data['category']); $this->storeBudgetWithJournal($journal, $data['budget_id']); - $accounts = $this->storeAccounts($journal->transactionType, $data); - $sourceAmount = bcmul(strval($data['amount']), '-1'); - $this->updateSourceTransaction($journal, $accounts['source'], $sourceAmount); // negative because source loses money. - $amount = strval($data['amount']); + $this->updateSourceTransaction($journal, $accounts['source'], bcmul($amount, '-1')); // negative because source loses money. $this->updateDestinationTransaction($journal, $accounts['destination'], $amount); // positive because destination gets money. $journal->save(); diff --git a/app/Support/ExpandedForm.php b/app/Support/ExpandedForm.php index bbaa657a4e..dab2ed286d 100644 --- a/app/Support/ExpandedForm.php +++ b/app/Support/ExpandedForm.php @@ -40,6 +40,8 @@ class ExpandedForm */ public function amount(string $name, $value = null, array $options = []): string { + $options['min'] = '0.01'; + return $this->currencyField($name, 'amount', $value, $options); } @@ -52,6 +54,8 @@ class ExpandedForm */ public function amountSmall(string $name, $value = null, array $options = []): string { + $options['min'] = '0.01'; + return $this->currencyField($name, 'amount-small', $value, $options); } diff --git a/public/js/ff/transactions/single/create.js b/public/js/ff/transactions/single/create.js index 25fc79f65f..5b7388693e 100644 --- a/public/js/ff/transactions/single/create.js +++ b/public/js/ff/transactions/single/create.js @@ -43,7 +43,7 @@ $(document).ready(function () { }); function getExchangeRate() { - var accountId = $('select[name="source_account_id"]').val(); + var accountId = getAccountId(); var selectedCurrencyId = parseInt($('input[name="amount_currency_id_amount"]').val()); var accountCurrencyId = parseInt(accountInfo[accountId].preferredCurrency); var selectedCurrencyCode = currencyInfo[selectedCurrencyId].code; @@ -53,7 +53,6 @@ function getExchangeRate() { var uri = 'json/rate/' + selectedCurrencyCode + '/' + accountCurrencyCode + '/' + date + '?amount=' + amount; console.log('Will grab ' + uri); $.get(uri).done(updateExchangedAmount); - } function updateExchangedAmount(data) { @@ -65,7 +64,7 @@ function updateExchangedAmount(data) { function triggerCurrencyChange() { var selectedCurrencyId = parseInt($('input[name="amount_currency_id_amount"]').val()); - var accountId = $('select[name="source_account_id"]').val(); + var accountId = getAccountId(); var accountCurrencyId = parseInt(accountInfo[accountId].preferredCurrency); console.log('Selected currency is ' + selectedCurrencyId); console.log('Account prefers ' + accountCurrencyId); @@ -93,7 +92,7 @@ function triggerCurrencyChange() { function updateCurrency() { // get value: - var accountId = $('select[name="source_account_id"]').val(); + var accountId = getAccountId(); var currencyPreference = accountInfo[accountId].preferredCurrency; $('.currency-option[data-id="' + currencyPreference + '"]').click(); @@ -253,4 +252,14 @@ function clickButton(e) { updateDescription(); } return false; +} + +/** + * Get accountID based on some meta info. + */ +function getAccountId() { + if(what === "withdrawal") { + return $('select[name="source_account_id"]').val(); + } + alert('Cannot handle ' + what); } \ No newline at end of file diff --git a/public/js/ff/transactions/single/edit.js b/public/js/ff/transactions/single/edit.js index a025212d43..0b632f9992 100644 --- a/public/js/ff/transactions/single/edit.js +++ b/public/js/ff/transactions/single/edit.js @@ -59,4 +59,81 @@ $(document).ready(function () { $('input[name="category"]').typeahead({source: data}); }); + $('.currency-option').on('click', triggerCurrencyChange); + + // always update the exchanged_amount to match the correct currency + var journalCurrency = currencyInfo[journal.transaction_currency_id].symbol; + $('.non-selectable-currency-symbol').text(journalCurrency); + + // hide the exchange amount / foreign things: + if (journal.transaction_currency_id === journalData.currency.id) { + $('#exchange_rate_instruction_holder').hide(); + $('#exchanged_amount_holder').hide(); + } + + // or update the related text. + if (journal.transaction_currency_id !== journalData.currency.id) { + // update info text: + var accountId = getAccountId(); + var text = exchangeRateInstructions.replace('@name', accountInfo[accountId].name); + text = text.replace(/@account_currency/g, currencyInfo[journal.transaction_currency_id].name); + text = text.replace(/@transaction_currency/g, currencyInfo[journalData.currency.id].name); + $('#ffInput_exchange_rate_instruction').text(text); + } }); + +function triggerCurrencyChange() { + var selectedCurrencyId = parseInt($('input[name="amount_currency_id_amount"]').val()); + var accountId = getAccountId(); + var accountCurrencyId = parseInt(accountInfo[accountId].preferredCurrency); + console.log('Selected currency is ' + selectedCurrencyId); + console.log('Account prefers ' + accountCurrencyId); + if (selectedCurrencyId !== accountCurrencyId) { + var text = exchangeRateInstructions.replace('@name', accountInfo[accountId].name); + text = text.replace(/@account_currency/g, currencyInfo[accountCurrencyId].name); + text = text.replace(/@transaction_currency/g, currencyInfo[selectedCurrencyId].name); + $('.non-selectable-currency-symbol').text(currencyInfo[accountCurrencyId].symbol); + getExchangeRate(); + + $('#ffInput_exchange_rate_instruction').text(text); + $('#exchange_rate_instruction_holder').show(); + $('#exchanged_amount_holder').show(); + } + if (selectedCurrencyId === accountCurrencyId) { + $('#exchange_rate_instruction_holder').hide(); + $('#exchanged_amount_holder').hide(); + } + + // if the value of the selected currency does not match the account's currency + // show the exchange rate thing! + return false; +} + +function getExchangeRate() { + var accountId = getAccountId(); + var selectedCurrencyId = parseInt($('input[name="amount_currency_id_amount"]').val()); + var accountCurrencyId = parseInt(accountInfo[accountId].preferredCurrency); + var selectedCurrencyCode = currencyInfo[selectedCurrencyId].code; + var accountCurrencyCode = currencyInfo[accountCurrencyId].code; + var date = $('#ffInput_date').val(); + var amount = $('#ffInput_amount').val(); + var uri = 'json/rate/' + selectedCurrencyCode + '/' + accountCurrencyCode + '/' + date + '?amount=' + amount; + console.log('Will grab ' + uri); + $.get(uri).done(updateExchangedAmount); +} + +function updateExchangedAmount(data) { + console.log('Returned data:'); + console.log(data); + $('#ffInput_exchanged_amount').val(data.amount); +} + +/** + * Get accountID based on some meta info. + */ +function getAccountId() { + if(journal.transaction_type.type === "Withdrawal") { + return $('select[name="source_account_id"]').val(); + } + alert('Cannot handle ' + journal.transaction_type.type); +} diff --git a/resources/views/transactions/show.twig b/resources/views/transactions/show.twig index 8af9ec6b6b..69d7fef5d5 100644 --- a/resources/views/transactions/show.twig +++ b/resources/views/transactions/show.twig @@ -36,7 +36,15 @@ {{ 'total_amount'|_ }} - {{ journal|formatJournal }} + {{ journal|formatJournal }} + {% if journal.hasMeta('original_amount') %} + {% if journal.transactiontype.type == 'Withdrawal' %} + ({{ formatAnything(originalCurrency, journal.getMeta('original_amount')*-1) }}) + {% else %} + ({{ formatAnything(originalCurrency, journal.getMeta('original_amount')) }}) + {% endif %} + {% endif %} + {{ trans('list.date') }} diff --git a/resources/views/transactions/single/edit.twig b/resources/views/transactions/single/edit.twig index 82149d1d50..ee02df0f71 100644 --- a/resources/views/transactions/single/edit.twig +++ b/resources/views/transactions/single/edit.twig @@ -46,24 +46,28 @@ {{ ExpandedForm.text('source_account_name',data.source_account_name, {label: trans('form.revenue_account')}) }} {% endif %} - + {# FREE FORMAT DESTINATION ACCOUNT ONLY FOR EXPENSES #} {% if what == 'withdrawal' %} {{ ExpandedForm.text('destination_account_name',data.destination_account_name, {label: trans('form.expense_account')}) }} {% endif %} - + {# SELECTABLE DESTINATION ACCOUNT ONLY FOR TRANSFERS AND DEPOSITS #} {% if what == 'transfer' or what == 'deposit' %} {{ ExpandedForm.select('destination_account_id',assetAccounts, data.destination_account_id, {label: trans('form.asset_destination_account')} ) }} {% endif %} - - {{ ExpandedForm.amount('amount',data.amount,{'currency' : journal.transactionCurrency}) }} + {# ALWAYS SHOW AMOUNT #} + {{ ExpandedForm.amount('amount',data.amount,{'currency' : data.currency}) }} - + {# INSTRUCTIONS FOR EXCHANGE RATES #} + {{ ExpandedForm.staticText('exchange_rate_instruction','(here be text)') }} + + {{ ExpandedForm.nonSelectableAmount('exchanged_amount') }} + + {# ALWAYS SHOW DATE #} {{ ExpandedForm.date('date',data['date']) }} -
@@ -235,6 +239,13 @@ + + + {% endblock %}