Merge branch 'release/4.4.0'

This commit is contained in:
James Cole
2017-04-23 19:07:19 +02:00
517 changed files with 3682 additions and 1763 deletions

View File

@@ -41,6 +41,8 @@ SHOW_INCOMPLETE_TRANSLATIONS=false
CACHE_PREFIX=firefly
EXCHANGE_RATE_SERVICE=fixerio
GOOGLE_MAPS_API_KEY=
ANALYTICS_ID=
SITE_OWNER=mail@example.com

View File

@@ -2,7 +2,15 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## [4.4.0] - 2017-04-23
### Added
- Firefly III can now handle foreign currencies better, including some code to get the exchange rate live from the web.
- Can now make rules for attachments, see #608, as suggested by dzaikos.
### Fixed
- Fixed #629, reported by forcaeluz
- Fixed #630, reported by welbert
- And more various bug fixes.
## [4.3.8] - 2017-04-08
@@ -203,13 +211,6 @@ An intermediate release because something in the Twig and Twigbridge libraries i
- Updated all email messages.
- Made some fonts local
### Deprecated
- Initial release.
### Removed
- Initial release.
### Fixed
- Issue #408
- Various issues with split journals
@@ -218,11 +219,6 @@ An intermediate release because something in the Twig and Twigbridge libraries i
- Issue #422, thx [xzaz](https://github.com/xzaz)
- Various import bugs, such as #416 ([zjean](https://github.com/zjean))
### Security
- Initial release.
## [4.1.7] - 2016-11-19
### Added
- Check for database table presence in console commands.
@@ -345,15 +341,6 @@ An intermediate release because something in the Twig and Twigbridge libraries i
- New Presidents Choice specific to fix #307
- Added some trimming (#335)
### Changed
- Initial release.
### Deprecated
- Initial release.
### Removed
- Initial release.
### Fixed
- Fixed a bug where incoming transactions would not be properly filtered in several reports.
- #334 by [cyberkov](https://github.com/cyberkov)
@@ -361,12 +348,6 @@ An intermediate release because something in the Twig and Twigbridge libraries i
- #336
- #338 found by [roberthorlings](https://github.com/roberthorlings)
### Security
- Initial release.
## [4.0.0] - 2015-09-26
### Added
- Upgraded to Laravel 5.3, most other libraries upgraded as well.

View File

@@ -1,63 +0,0 @@
<?php
/**
* ConfigureLogging.php
* Copyright (C) 2016 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.
*/
declare(strict_types = 1);
namespace FireflyIII\Bootstrap;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Foundation\Bootstrap\ConfigureLogging as IlluminateConfigureLogging;
use Illuminate\Log\Writer;
/**
* Class ConfigureLogging
*
* @package FireflyIII\Bootstrap
*/
class ConfigureLogging extends IlluminateConfigureLogging
{
/**
* Configure the Monolog handlers for the application.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @param \Illuminate\Log\Writer $log
*
* @return void
*/
protected function configureDailyHandler(Application $app, Writer $log)
{
$config = $app->make('config');
$maxFiles = $config->get('app.log_max_files');
$log->useDailyFiles(
$app->storagePath() . '/logs/firefly-iii.log', is_null($maxFiles) ? 5 : $maxFiles,
$config->get('app.log_level', 'debug')
);
}
/**
* Configure the Monolog handlers for the application.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @param \Illuminate\Log\Writer $log
*
* @return void
*/
protected function configureSingleHandler(Application $app, Writer $log)
{
$log->useFiles(
$app->storagePath() . '/logs/firefly-iii.log',
$app->make('config')->get('app.log_level', 'debug')
);
}
}

View File

@@ -15,15 +15,22 @@ namespace FireflyIII\Console\Commands;
use DB;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountMeta;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\BudgetLimit;
use FireflyIII\Models\LimitRepetition;
use FireflyIII\Models\PiggyBankEvent;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use Illuminate\Console\Command;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Database\QueryException;
use Log;
use Preferences;
use Schema;
/**
@@ -63,8 +70,14 @@ class UpgradeDatabase extends Command
$this->setTransactionIdentifier();
$this->migrateRepetitions();
$this->repairPiggyBanks();
$this->updateAccountCurrencies();
$this->updateJournalCurrencies();
$this->info('Firefly III database is up to date.');
}
/**
* Migrate budget repetitions to new format.
*/
private function migrateRepetitions()
{
if (!Schema::hasTable('budget_limits')) {
@@ -102,18 +115,20 @@ class UpgradeDatabase extends Command
/** @var PiggyBankEvent $event */
foreach ($set as $event) {
if (!is_null($event->transaction_journal_id)) {
$type = $event->transactionJournal->transactionType->type;
if (is_null($event->transaction_journal_id)) {
continue;
}
/** @var TransactionJournal $journal */
$journal = $event->transactionJournal()->first();
if (is_null($journal)) {
continue;
}
$type = $journal->transactionType->type;
if ($type !== TransactionType::TRANSFER) {
$event->transaction_journal_id = null;
$event->save();
$this->line(
sprintf('Piggy bank #%d ("%s") was referenced by an invalid event. This has been fixed.', $event->piggy_bank_id,
$event->piggyBank->name
));
}
$this->line(sprintf('Piggy bank #%d was referenced by an invalid event. This has been fixed.', $event->piggy_bank_id));
}
}
}
@@ -146,6 +161,57 @@ class UpgradeDatabase extends Command
}
}
/**
*
*/
private function updateAccountCurrencies()
{
$accounts = Account::leftJoin('account_types', 'account_types.id', '=', 'accounts.account_type_id')
->whereIn('account_types.type', [AccountType::DEFAULT, AccountType::ASSET])->get(['accounts.*']);
/** @var Account $account */
foreach ($accounts as $account) {
// get users preference, fall back to system pref.
$defaultCurrencyCode = Preferences::getForUser($account->user, 'currencyPreference', config('firefly.default_currency', 'EUR'))->data;
$defaultCurrency = TransactionCurrency::where('code', $defaultCurrencyCode)->first();
$accountCurrency = intval($account->getMeta('currency_id'));
$openingBalance = $account->getOpeningBalance();
$openingBalanceCurrency = intval($openingBalance->transaction_currency_id);
// both 0? set to default currency:
if ($accountCurrency === 0 && $openingBalanceCurrency === 0) {
AccountMeta::create(['account_id' => $account->id, 'name' => 'currency_id', 'data' => $defaultCurrency->id]);
$this->line(sprintf('Account #%d ("%s") now has a currency setting (%s).', $account->id, $account->name, $defaultCurrencyCode));
continue;
}
// opening balance 0, account not zero? just continue:
if ($accountCurrency > 0 && $openingBalanceCurrency === 0) {
continue;
}
// account is set to 0, opening balance is not?
if ($accountCurrency === 0 && $openingBalanceCurrency > 0) {
AccountMeta::create(['account_id' => $account->id, 'name' => 'currency_id', 'data' => $openingBalanceCurrency]);
$this->line(sprintf('Account #%d ("%s") now has a currency setting (%s).', $account->id, $account->name, $defaultCurrencyCode));
continue;
}
// both are equal, just continue:
if ($accountCurrency === $openingBalanceCurrency) {
continue;
}
// do not match:
if ($accountCurrency !== $openingBalanceCurrency) {
// update opening balance:
$openingBalance->transaction_currency_id = $accountCurrency;
$openingBalance->save();
$this->line(sprintf('Account #%d ("%s") now has a correct currency for opening balance.', $account->id, $account->name));
continue;
}
}
}
/**
* grab all positive transactiosn from this journal that are not deleted. for each one, grab the negative opposing one
* which has 0 as an identifier and give it the same identifier.
@@ -189,4 +255,83 @@ class UpgradeDatabase extends Command
$identifier++;
}
}
/**
* Makes sure that withdrawals, deposits and transfers have
* a currency setting matching their respective accounts
*/
private function updateJournalCurrencies()
{
$types = [
TransactionType::WITHDRAWAL => '<',
TransactionType::DEPOSIT => '>',
];
$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.';
foreach ($types as $type => $operator) {
$set = 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');
}
)
->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']);
/** @var TransactionJournal $journal */
foreach ($set as $journal) {
$expectedCurrency = $repository->find(intval($journal->expected_currency_id));
$line = sprintf($notification, $type, $journal->id, $journal->transactionCurrency->code, $expectedCurrency->code);
$journal->setMeta('foreign_amount', $journal->transaction_amount);
$journal->setMeta('foreign_currency_id', $journal->transaction_currency_id);
$journal->transaction_currency_id = $expectedCurrency->id;
$journal->save();
$this->line($line);
}
}
/*
* For transfers it's slightly different. Both source and destination
* must match the respective currency preference. So we must verify ALL
* transactions.
*/
$set = TransactionJournal
::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
->where('transaction_types.type', TransactionType::TRANSFER)
->get(['transaction_journals.*']);
/** @var TransactionJournal $journal */
foreach ($set as $journal) {
$updated = false;
/** @var Transaction $sourceTransaction */
$sourceTransaction = $journal->transactions()->where('amount', '<', 0)->first();
$sourceCurrency = $repository->find(intval($sourceTransaction->account->getMeta('currency_id')));
if ($sourceCurrency->id !== $journal->transaction_currency_id) {
$updated = true;
$journal->transaction_currency_id = $sourceCurrency->id;
$journal->save();
}
// destination
$destinationTransaction = $journal->transactions()->where('amount', '>', 0)->first();
$destinationCurrency = $repository->find(intval($destinationTransaction->account->getMeta('currency_id')));
if ($destinationCurrency->id !== $journal->transaction_currency_id) {
$updated = true;
$journal->deleteMeta('foreign_amount');
$journal->deleteMeta('foreign_currency_id');
$journal->setMeta('foreign_amount', $destinationTransaction->amount);
$journal->setMeta('foreign_currency_id', $destinationCurrency->id);
}
if ($updated) {
$line = sprintf($transfer, $journal->id);
$this->line($line);
}
}
}
}

View File

@@ -41,7 +41,6 @@ class Kernel extends ConsoleKernel
= [
'Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables',
'Illuminate\Foundation\Bootstrap\LoadConfiguration',
//'FireflyIII\Bootstrap\ConfigureLogging',
'Illuminate\Foundation\Bootstrap\HandleExceptions',
'Illuminate\Foundation\Bootstrap\RegisterFacades',
'Illuminate\Foundation\Bootstrap\SetRequestForConsole',

View File

@@ -10,6 +10,7 @@
*/
declare(strict_types=1);
namespace FireflyIII\Events;
/**

View File

@@ -10,6 +10,7 @@
*/
declare(strict_types=1);
namespace FireflyIII\Exceptions;

View File

@@ -10,6 +10,7 @@
*/
declare(strict_types=1);
namespace FireflyIII\Exceptions;
use ErrorException;

View File

@@ -10,6 +10,7 @@
*/
declare(strict_types=1);
namespace FireflyIII\Exceptions;

View File

@@ -10,6 +10,7 @@
*/
declare(strict_types=1);
namespace FireflyIII\Exceptions;
/**

View File

@@ -81,6 +81,9 @@ class ChartJsGenerator implements GeneratorInterface
if (isset($set['fill'])) {
$currentSet['fill'] = $set['fill'];
}
if (isset($set['currency_symbol'])) {
$currentSet['currency_symbol'] = $set['currency_symbol'];
}
$chartData['datasets'][] = $currentSet;
}
@@ -105,6 +108,10 @@ class ChartJsGenerator implements GeneratorInterface
],
'labels' => [],
];
// sort by value, keep keys.
asort($data);
$index = 0;
foreach ($data as $key => $value) {

View File

@@ -10,6 +10,7 @@
*/
declare(strict_types=1);
namespace FireflyIII\Helpers\Attachments;
use Crypt;

View File

@@ -10,6 +10,7 @@
*/
declare(strict_types=1);
namespace FireflyIII\Helpers\Collection;
use Illuminate\Support\Collection;

View File

@@ -10,6 +10,7 @@
*/
declare(strict_types=1);
namespace FireflyIII\Helpers\Collection;
use FireflyIII\Models\Account as AccountModel;

View File

@@ -10,6 +10,7 @@
*/
declare(strict_types=1);
namespace FireflyIII\Helpers\Collection;
use FireflyIII\Models\Account as AccountModel;

View File

@@ -10,6 +10,7 @@
*/
declare(strict_types=1);
namespace FireflyIII\Helpers\Collection;
use Carbon\Carbon;

View File

@@ -10,6 +10,7 @@
*/
declare(strict_types=1);
namespace FireflyIII\Helpers\Collection;

View File

@@ -10,6 +10,7 @@
*/
declare(strict_types=1);
namespace FireflyIII\Helpers\Collection;
use Carbon\Carbon;

View File

@@ -10,6 +10,7 @@
*/
declare(strict_types=1);
namespace FireflyIII\Helpers\Collection;
use FireflyIII\Models\Category as CategoryModel;

View File

@@ -10,6 +10,7 @@
*/
declare(strict_types=1);
namespace FireflyIII\Helpers\Help;
/**

View File

@@ -28,6 +28,37 @@ class PopupReport implements PopupReportInterface
{
/**
* @param $account
* @param $attributes
*
* @return Collection
*/
public function balanceDifference($account, $attributes): Collection
{
// row that displays difference
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
$collector
->setAccounts(new Collection([$account]))
->setTypes([TransactionType::WITHDRAWAL])
->setRange($attributes['startDate'], $attributes['endDate'])
->withoutBudget();
$journals = $collector->getJournals();
return $journals->filter(
function (Transaction $transaction) {
$tags = $transaction->transactionJournal->tags()->where('tagMode', 'balancingAct')->count();
if ($tags === 0) {
return true;
}
return false;
}
);
}
/**
* @param Budget $budget
* @param Account $account
@@ -165,35 +196,4 @@ class PopupReport implements PopupReportInterface
return $journals;
}
/**
* @param $account
* @param $attributes
*
* @return Collection
*/
public function balanceDifference($account, $attributes): Collection
{
// row that displays difference
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
$collector
->setAccounts(new Collection([$account]))
->setTypes([TransactionType::WITHDRAWAL])
->setRange($attributes['startDate'], $attributes['endDate'])
->withoutBudget();
$journals = $collector->getJournals();
return $journals->filter(
function (Transaction $transaction) {
$tags = $transaction->transactionJournal->tags()->where('tagMode', 'balancingAct')->count();
if ($tags === 0) {
return true;
}
return false;
}
);
}
}

View File

@@ -22,8 +22,8 @@ use FireflyIII\Http\Requests\AccountFormRequest;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Account\AccountTaskerInterface;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use FireflyIII\Repositories\Journal\JournalRepositoryInterface;
use FireflyIII\Support\CacheProperties;
@@ -70,7 +70,8 @@ class AccountController extends Controller
{
/** @var CurrencyRepositoryInterface $repository */
$repository = app(CurrencyRepositoryInterface::class);
$currencies = ExpandedForm::makeSelectList($repository->get());
$allCurrencies = $repository->get();
$currencySelectList = ExpandedForm::makeSelectList($allCurrencies);
$defaultCurrency = Amount::getDefaultCurrency();
$subTitleIcon = config('firefly.subIconsByIdentifier.' . $what);
$subTitle = trans('firefly.make_new_' . $what . '_account');
@@ -91,7 +92,7 @@ class AccountController extends Controller
$request->session()->flash('gaEventCategory', 'accounts');
$request->session()->flash('gaEventAction', 'create-' . $what);
return view('accounts.create', compact('subTitleIcon', 'what', 'subTitle', 'currencies', 'roles'));
return view('accounts.create', compact('subTitleIcon', 'what', 'subTitle', 'currencySelectList', 'allCurrencies', 'roles'));
}
@@ -147,13 +148,13 @@ class AccountController extends Controller
*/
public function edit(Request $request, Account $account)
{
/** @var CurrencyRepositoryInterface $repository */
$repository = app(CurrencyRepositoryInterface::class);
$what = config('firefly.shortNamesByFullName')[$account->accountType->type];
$subTitle = trans('firefly.edit_' . $what . '_account', ['name' => $account->name]);
$subTitleIcon = config('firefly.subIconsByIdentifier.' . $what);
/** @var CurrencyRepositoryInterface $repository */
$repository = app(CurrencyRepositoryInterface::class);
$currencies = ExpandedForm::makeSelectList($repository->get());
$allCurrencies = $repository->get();
$currencySelectList = ExpandedForm::makeSelectList($allCurrencies);
$roles = [];
foreach (config('firefly.accountRoles') as $role) {
$roles[$role] = strval(trans('firefly.account_role_' . $role));
@@ -173,6 +174,7 @@ class AccountController extends Controller
$openingBalanceAmount = $account->getOpeningBalanceAmount() === '0' ? '' : $openingBalanceAmount;
$openingBalanceDate = $account->getOpeningBalanceDate();
$openingBalanceDate = $openingBalanceDate->year === 1900 ? null : $openingBalanceDate->format('Y-m-d');
$currency = $repository->find(intval($account->getMeta('currency_id')));
$preFilled = [
'accountNumber' => $account->getMeta('accountNumber'),
@@ -183,13 +185,18 @@ class AccountController extends Controller
'openingBalanceDate' => $openingBalanceDate,
'openingBalance' => $openingBalanceAmount,
'virtualBalance' => $account->virtual_balance,
'currency_id' => $account->getMeta('currency_id'),
'currency_id' => $currency->id,
];
$request->session()->flash('preFilled', $preFilled);
$request->session()->flash('gaEventCategory', 'accounts');
$request->session()->flash('gaEventAction', 'edit-' . $what);
return view('accounts.edit', compact('currencies', 'account', 'subTitle', 'subTitleIcon', 'openingBalance', 'what', 'roles'));
return view(
'accounts.edit', compact(
'allCurrencies', 'currencySelectList', 'account', 'currency', 'subTitle', 'subTitleIcon', 'openingBalance', 'what', 'roles'
)
);
}
/**
@@ -242,6 +249,8 @@ class AccountController extends Controller
if ($account->accountType->type === AccountType::INITIAL_BALANCE) {
return $this->redirectToOriginalAccount($account);
}
/** @var CurrencyRepositoryInterface $currencyRepos */
$currencyRepos = app(CurrencyRepositoryInterface::class);
$range = Preferences::get('viewRange', '1M')->data;
$subTitleIcon = config('firefly.subIconsByIdentifier.' . $account->accountType->type);
$page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page'));
@@ -250,6 +259,7 @@ class AccountController extends Controller
$start = null;
$end = null;
$periods = new Collection;
$currency = $currencyRepos->find(intval($account->getMeta('currency_id')));
// prep for "all" view.
if ($moment === 'all') {
@@ -283,7 +293,6 @@ class AccountController extends Controller
$periods = $this->getPeriodOverview($account);
}
$accountType = $account->accountType->type;
$count = 0;
$loop = 0;
// grab journals, but be prepared to jump a period back to get the right ones:
@@ -316,7 +325,8 @@ class AccountController extends Controller
return view(
'accounts.show', compact('account', 'moment', 'accountType', 'periods', 'subTitleIcon', 'journals', 'subTitle', 'start', 'end', 'chartUri')
'accounts.show',
compact('account', 'currency', 'moment', 'periods', 'subTitleIcon', 'journals', 'subTitle', 'start', 'end', 'chartUri')
);
}
@@ -331,7 +341,6 @@ class AccountController extends Controller
{
$data = $request->getAccountData();
$account = $repository->store($data);
$request->session()->flash('success', strval(trans('firefly.stored_new_account', ['name' => $account->name])));
Preferences::mark();
@@ -409,9 +418,6 @@ class AccountController extends Controller
{
/** @var AccountRepositoryInterface $repository */
$repository = app(AccountRepositoryInterface::class);
/** @var AccountTaskerInterface $tasker */
$tasker = app(AccountTaskerInterface::class);
$start = $repository->oldestJournalDate($account);
$range = Preferences::get('viewRange', '1M')->data;
$start = Navigation::startOfPeriod($start, $range);
@@ -429,17 +435,26 @@ class AccountController extends Controller
return $cache->get(); // @codeCoverageIgnore
}
// only include asset accounts when this account is an asset:
$assets = new Collection;
if (in_array($account->accountType->type, [AccountType::ASSET, AccountType::DEFAULT])) {
$assets = $repository->getAccountsByType([AccountType::ASSET, AccountType::DEFAULT]);
}
Log::debug('Going to get period expenses and incomes.');
while ($end >= $start) {
$end = Navigation::startOfPeriod($end, $range);
$currentEnd = Navigation::endOfPeriod($end, $range);
$spent = $tasker->amountOutInPeriod(new Collection([$account]), $assets, $end, $currentEnd);
$earned = $tasker->amountInInPeriod(new Collection([$account]), $assets, $end, $currentEnd);
// try a collector for income:
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
$collector->setAccounts(new Collection([$account]))->setRange($end, $currentEnd)
->setTypes([TransactionType::DEPOSIT])
->withOpposingAccount();
$earned = strval($collector->getJournals()->sum('transaction_amount'));
// try a collector for expenses:
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
$collector->setAccounts(new Collection([$account]))->setRange($end, $currentEnd)
->setTypes([TransactionType::WITHDRAWAL])
->withOpposingAccount();
$spent = strval($collector->getJournals()->sum('transaction_amount'));
$dateStr = $end->format('Y-m-d');
$dateName = Navigation::periodShow($end, $range);
$entries->push(

View File

@@ -128,6 +128,8 @@ class UserController extends Controller
* @param UserFormRequest $request
* @param User $user
*
* @param UserRepositoryInterface $repository
*
* @return $this|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function update(UserFormRequest $request, User $user, UserRepositoryInterface $repository)

View File

@@ -43,6 +43,8 @@ class ForgotPasswordController extends Controller
*
* @param Request $request
*
* @param UserRepositoryInterface $repository
*
* @return \Illuminate\Http\RedirectResponse
*/
public function sendResetLinkEmail(Request $request, UserRepositoryInterface $repository)

View File

@@ -112,6 +112,8 @@ class LoginController extends Controller
*
* @param Request $request
*
* @param CookieJar $cookieJar
*
* @return \Illuminate\Http\Response
*/
public function showLoginForm(Request $request, CookieJar $cookieJar)

View File

@@ -192,6 +192,7 @@ class BudgetController extends Controller
/**
* @param Request $request
* @param JournalRepositoryInterface $repository
* @param string $moment
*
* @return View
@@ -310,7 +311,7 @@ class BudgetController extends Controller
$journals->setPath('/budgets/show/' . $budget->id);
$subTitle = e($budget->name);
$subTitle = trans('firefly.all_journals_for_budget', ['name' => $budget->name]);
return view('budgets.show', compact('limits', 'budget', 'repetition', 'journals', 'subTitle'));
}

View File

@@ -154,6 +154,10 @@ class CategoryController extends Controller
}
/**
* @param Request $request
* @param JournalRepositoryInterface $repository
* @param string $moment
*
* @return View
*/
public function noCategory(Request $request, JournalRepositoryInterface $repository, string $moment = '')
@@ -398,7 +402,7 @@ class CategoryController extends Controller
// count journals without budget in this period:
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($end, $currentEnd)->withoutCategory()->withOpposingAccount();
$collector->setAllAssetAccounts()->setRange($end, $currentEnd)->withoutCategory()->withOpposingAccount()->disableInternalFilter();
$count = $collector->getJournals()->count();
// amount transferred

View File

@@ -26,6 +26,7 @@ use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Repositories\Category\CategoryRepositoryInterface;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use FireflyIII\Support\CacheProperties;
use Illuminate\Support\Collection;
use Log;
@@ -335,19 +336,13 @@ class AccountController extends Controller
/**
* @param Account $account
* @param string $date
* @param Carbon $start
*
* @return \Illuminate\Http\JsonResponse
* @throws FireflyException
*/
public function period(Account $account, string $date)
public function period(Account $account, Carbon $start)
{
try {
$start = new Carbon($date);
} catch (Exception $e) {
Log::error($e->getMessage());
throw new FireflyException('"' . e($date) . '" does not seem to be a valid date. Should be in the format YYYY-MM-DD');
}
$range = Preferences::get('viewRange', '1M')->data;
$end = Navigation::endOfPeriod($start, $range);
$cache = new CacheProperties();
@@ -501,10 +496,15 @@ class AccountController extends Controller
}
Log::debug('Regenerate chart.account.account-balance-chart from scratch.');
/** @var CurrencyRepositoryInterface $repository */
$repository = app(CurrencyRepositoryInterface::class);
$chartData = [];
foreach ($accounts as $account) {
$currency = $repository->find(intval($account->getMeta('currency_id')));
$currentSet = [
'label' => $account->name,
'currency_symbol' => $currency->symbol,
'entries' => [],
];
$currentStart = clone $start;

View File

@@ -18,11 +18,14 @@ use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Generator\Chart\Basic\GeneratorInterface;
use FireflyIII\Helpers\Collector\JournalCollectorInterface;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\Budget;
use FireflyIII\Models\BudgetLimit;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Repositories\Category\CategoryRepositoryInterface;
use FireflyIII\Support\CacheProperties;
use Illuminate\Support\Collection;
use Navigation;
@@ -153,6 +156,144 @@ class BudgetController extends Controller
return Response::json($data);
}
/**
* @param Budget $budget
* @param BudgetLimit|null $budgetLimit
*
* @return \Illuminate\Http\JsonResponse
*/
public function expenseAsset(Budget $budget, BudgetLimit $budgetLimit = null)
{
$cache = new CacheProperties;
$cache->addProperty($budget->id);
$cache->addProperty($budgetLimit->id ?? 0);
$cache->addProperty('chart.budget.expense-asset');
if ($cache->has()) {
return Response::json($cache->get()); // @codeCoverageIgnore
}
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
$collector->setAllAssetAccounts()->setTypes([TransactionType::WITHDRAWAL])->setBudget($budget);
if (!is_null($budgetLimit->id)) {
$collector->setRange($budgetLimit->start_date, $budgetLimit->end_date);
}
$transactions = $collector->getJournals();
$result = [];
$chartData = [];
/** @var Transaction $transaction */
foreach ($transactions as $transaction) {
$assetId = intval($transaction->account_id);
$result[$assetId] = $result[$assetId] ?? '0';
$result[$assetId] = bcadd($transaction->transaction_amount, $result[$assetId]);
}
$names = $this->getAccountNames(array_keys($result));
foreach ($result as $assetId => $amount) {
$chartData[$names[$assetId]] = $amount;
}
$data = $this->generator->pieChart($chartData);
$cache->store($data);
return Response::json($data);
}
/**
* @param Budget $budget
* @param BudgetLimit|null $budgetLimit
*
* @return \Illuminate\Http\JsonResponse
*/
public function expenseCategory(Budget $budget, BudgetLimit $budgetLimit = null)
{
$cache = new CacheProperties;
$cache->addProperty($budget->id);
$cache->addProperty($budgetLimit->id ?? 0);
$cache->addProperty('chart.budget.expense-category');
if ($cache->has()) {
return Response::json($cache->get()); // @codeCoverageIgnore
}
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
$collector->setAllAssetAccounts()->setTypes([TransactionType::WITHDRAWAL])->setBudget($budget)->withCategoryInformation();
if (!is_null($budgetLimit->id)) {
$collector->setRange($budgetLimit->start_date, $budgetLimit->end_date);
}
$transactions = $collector->getJournals();
$result = [];
$chartData = [];
/** @var Transaction $transaction */
foreach ($transactions as $transaction) {
$jrnlCatId = intval($transaction->transaction_journal_category_id);
$transCatId = intval($transaction->transaction_category_id);
$categoryId = max($jrnlCatId, $transCatId);
$result[$categoryId] = $result[$categoryId] ?? '0';
$result[$categoryId] = bcadd($transaction->transaction_amount, $result[$categoryId]);
}
$names = $this->getCategoryNames(array_keys($result));
foreach ($result as $categoryId => $amount) {
$chartData[$names[$categoryId]] = $amount;
}
$data = $this->generator->pieChart($chartData);
$cache->store($data);
return Response::json($data);
}
/**
* @param Budget $budget
* @param BudgetLimit|null $budgetLimit
*
* @return \Illuminate\Http\JsonResponse
*/
public function expenseExpense(Budget $budget, BudgetLimit $budgetLimit = null)
{
$cache = new CacheProperties;
$cache->addProperty($budget->id);
$cache->addProperty($budgetLimit->id ?? 0);
$cache->addProperty('chart.budget.expense-expense');
if ($cache->has()) {
return Response::json($cache->get()); // @codeCoverageIgnore
}
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
$collector->setAllAssetAccounts()->setTypes([TransactionType::WITHDRAWAL])->setBudget($budget)->withOpposingAccount();
if (!is_null($budgetLimit->id)) {
$collector->setRange($budgetLimit->start_date, $budgetLimit->end_date);
}
$transactions = $collector->getJournals();
$result = [];
$chartData = [];
/** @var Transaction $transaction */
foreach ($transactions as $transaction) {
$opposingId = intval($transaction->opposing_account_id);
$result[$opposingId] = $result[$opposingId] ?? '0';
$result[$opposingId] = bcadd($transaction->transaction_amount, $result[$opposingId]);
}
$names = $this->getAccountNames(array_keys($result));
foreach ($result as $opposingId => $amount) {
$name = $names[$opposingId] ?? 'no name';
$chartData[$name] = $amount;
}
$data = $this->generator->pieChart($chartData);
$cache->store($data);
return Response::json($data);
}
/**
* Shows a budget list with spent/left/overspent.
* @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's exactly five.
@@ -288,6 +429,28 @@ class BudgetController extends Controller
return Response::json($data);
}
/**
* @param array $accountIds
*
* @return array
*/
private function getAccountNames(array $accountIds): array
{
/** @var AccountRepositoryInterface $repository */
$repository = app(AccountRepositoryInterface::class);
$accounts = $repository->getAccountsByType([AccountType::ASSET, AccountType::DEFAULT, AccountType::EXPENSE]);
$grouped = $accounts->groupBy('id')->toArray();
$return = [];
foreach ($accountIds as $accountId) {
if (isset($grouped[$accountId])) {
$return[$accountId] = $grouped[$accountId][0]['name'];
}
}
$return[0] = '(no name)';
return $return;
}
/**
* @param Budget $budget
* @param Carbon $start
@@ -314,6 +477,30 @@ class BudgetController extends Controller
return $budgeted;
}
/**
* Small helper function for some of the charts.
*
* @param array $categoryIds
*
* @return array
*/
private function getCategoryNames(array $categoryIds): array
{
/** @var CategoryRepositoryInterface $repository */
$repository = app(CategoryRepositoryInterface::class);
$categories = $repository->getCategories();
$grouped = $categories->groupBy('id')->toArray();
$return = [];
foreach ($categoryIds as $categoryId) {
if (isset($grouped[$categoryId])) {
$return[$categoryId] = $grouped[$categoryId][0]['name'];
}
}
$return[0] = trans('firefly.noCategory');
return $return;
}
/**
*
* @SuppressWarnings(PHPMD.CyclomaticComplexity) // it's 6 but ok.

View File

@@ -100,9 +100,9 @@ class CategoryController extends Controller
$earned = $repository->earnedInPeriod(new Collection([$category]), $accounts, $start, $currentEnd);
$sum = bcadd($spent, $earned);
$label = Navigation::periodShow($start, $range);
$chartData[0]['entries'][$label] = bcmul($spent, '-1');
$chartData[1]['entries'][$label] = $earned;
$chartData[2]['entries'][$label] = $sum;
$chartData[0]['entries'][$label] = round(bcmul($spent, '-1'), 12);
$chartData[1]['entries'][$label] = round($earned, 12);
$chartData[2]['entries'][$label] = round($sum, 12);
$start = Navigation::addPeriod($start, $range, 0);
}
@@ -113,21 +113,6 @@ class CategoryController extends Controller
}
/**
* @param CategoryRepositoryInterface $repository
* @param Category $category
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function currentPeriod(CategoryRepositoryInterface $repository, Category $category)
{
$start = clone session('start', Carbon::now()->startOfMonth());
$end = session('end', Carbon::now()->endOfMonth());
$data = $this->makePeriodChart($repository, $category, $start, $end);
return Response::json($data);
}
/**
* @param CategoryRepositoryInterface $repository
* @param AccountRepositoryInterface $accountRepository
@@ -215,9 +200,9 @@ class CategoryController extends Controller
$spent = $expenses[$category->id]['entries'][$period] ?? '0';
$earned = $income[$category->id]['entries'][$period] ?? '0';
$sum = bcadd($spent, $earned);
$chartData[0]['entries'][$label] = bcmul($spent, '-1');
$chartData[1]['entries'][$label] = $earned;
$chartData[2]['entries'][$label] = $sum;
$chartData[0]['entries'][$label] = round(bcmul($spent, '-1'), 12);
$chartData[1]['entries'][$label] = round($earned, 12);
$chartData[2]['entries'][$label] = round($sum, 12);
}
$data = $this->generator->multiSet($chartData);
@@ -290,12 +275,11 @@ class CategoryController extends Controller
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function specificPeriod(CategoryRepositoryInterface $repository, Category $category, $date)
public function specificPeriod(CategoryRepositoryInterface $repository, Category $category, Carbon $date)
{
$carbon = new Carbon($date);
$range = Preferences::get('viewRange', '1M')->data;
$start = Navigation::startOfPeriod($carbon, $range);
$end = Navigation::endOfPeriod($carbon, $range);
$start = Navigation::startOfPeriod($date, $range);
$end = Navigation::endOfPeriod($date, $range);
$data = $this->makePeriodChart($repository, $category, $start, $end);
return Response::json($data);
@@ -350,11 +334,11 @@ class CategoryController extends Controller
$spent = $repository->spentInPeriod(new Collection([$category]), $accounts, $start, $start);
$earned = $repository->earnedInPeriod(new Collection([$category]), $accounts, $start, $start);
$sum = bcadd($spent, $earned);
$label = Navigation::periodShow($start, '1D');
$label = trim(Navigation::periodShow($start, '1D'));
$chartData[0]['entries'][$label] = bcmul($spent, '-1');
$chartData[1]['entries'][$label] = $earned;
$chartData[2]['entries'][$label] = $sum;
$chartData[0]['entries'][$label] = round(bcmul($spent, '-1'),12);
$chartData[1]['entries'][$label] = round($earned,12);
$chartData[2]['entries'][$label] = round($sum,12);
$start->addDay();

View File

@@ -20,6 +20,7 @@ use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Repositories\Account\AccountTaskerInterface;
use FireflyIII\Support\CacheProperties;
use Illuminate\Support\Collection;
use Log;
use Navigation;
use Response;
use Steam;
@@ -103,8 +104,9 @@ class ReportController extends Controller
$cache->addProperty($accounts);
$cache->addProperty($end);
if ($cache->has()) {
return Response::json($cache->get()); // @codeCoverageIgnore
//return Response::json($cache->get()); // @codeCoverageIgnore
}
Log::debug('Going to do operations for accounts ', $accounts->pluck('id')->toArray());
$format = Navigation::preferredCarbonLocalizedFormat($start, $end);
$source = $this->getChartData($accounts, $start, $end);
$chartData = [
@@ -163,6 +165,8 @@ class ReportController extends Controller
if ($cache->has()) {
return Response::json($cache->get()); // @codeCoverageIgnore
}
$source = $this->getChartData($accounts, $start, $end);
$numbers = [
'sum_earned' => '0',
@@ -246,19 +250,41 @@ class ReportController extends Controller
$cache->addProperty($accounts);
$cache->addProperty($end);
if ($cache->has()) {
return $cache->get(); // @codeCoverageIgnore
// return $cache->get(); // @codeCoverageIgnore
}
$tasker = app(AccountTaskerInterface::class);
$currentStart = clone $start;
$spentArray = [];
$earnedArray = [];
/** @var AccountTaskerInterface $tasker */
$tasker = app(AccountTaskerInterface::class);
while ($currentStart <= $end) {
$currentEnd = Navigation::endOfPeriod($currentStart, '1M');
$earned = strval(
array_sum(
array_map(
function ($item) {
return $item['sum'];
}, $tasker->getIncomeReport($currentStart, $currentEnd, $accounts)
)
)
);
$spent = strval(
array_sum(
array_map(
function ($item) {
return $item['sum'];
}, $tasker->getExpenseReport($currentStart, $currentEnd, $accounts)
)
)
);
$label = $currentStart->format('Y-m') . '-01';
$spent = $tasker->amountOutInPeriod($accounts, $accounts, $currentStart, $currentEnd);
$earned = $tasker->amountInInPeriod($accounts, $accounts, $currentStart, $currentEnd);
$spentArray[$label] = bcmul($spent, '-1');
$earnedArray[$label] = $earned;
$currentStart = Navigation::addPeriod($currentStart, '1M', 0);

View File

@@ -233,7 +233,7 @@ class TagReportController extends Controller
}
}
if (count($newSet) === 0) {
$newSet = $chartData;
$newSet = $chartData; // @codeCoverageIgnore
}
$data = $this->generator->multiSet($newSet);
$cache->store($data);

View File

@@ -10,6 +10,7 @@
*/
declare(strict_types=1);
namespace FireflyIII\Http\Controllers;
use Artisan;

View File

@@ -274,10 +274,10 @@ class ImportController extends Controller
* Step 5. Depending on the importer, this will show the user settings to
* fill in.
*
* @param ImportJobRepositoryInterface $repository
* @param ImportJob $job
*
* @return View
* @throws FireflyException
*/
public function settings(ImportJobRepositoryInterface $repository, ImportJob $job)
{

View File

@@ -15,6 +15,7 @@ use Amount;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use Illuminate\Http\Request;
@@ -29,9 +30,11 @@ use Session;
*/
class JavascriptController extends Controller
{
/**
* @param AccountRepositoryInterface $repository
* @param CurrencyRepositoryInterface $currencyRepository
*
* @return $this
*/
public function accounts(AccountRepositoryInterface $repository, CurrencyRepositoryInterface $currencyRepository)
{
@@ -47,7 +50,7 @@ class JavascriptController extends Controller
$accountId = $account->id;
$currency = intval($account->getMeta('currency_id'));
$currency = $currency === 0 ? $default->id : $currency;
$entry = ['preferredCurrency' => $currency];
$entry = ['preferredCurrency' => $currency, 'name' => $account->name];
$data['accounts'][$accountId] = $entry;
}
@@ -57,6 +60,27 @@ class JavascriptController extends Controller
->header('Content-Type', 'text/javascript');
}
/**
* @param CurrencyRepositoryInterface $repository
*
* @return $this
*/
public function currencies(CurrencyRepositoryInterface $repository)
{
$currencies = $repository->get();
$data = ['currencies' => [],];
/** @var TransactionCurrency $currency */
foreach ($currencies as $currency) {
$currencyId = $currency->id;
$entry = ['name' => $currency->name, 'code' => $currency->code, 'symbol' => $currency->symbol];
$data['currencies'][$currencyId] = $entry;
}
return response()
->view('javascript.currencies', $data, 200)
->header('Content-Type', 'text/javascript');
}
/**
* @param Request $request
*

View File

@@ -0,0 +1,66 @@
<?php
/**
* ExchangeController.php
* 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.
*/
declare(strict_types=1);
namespace FireflyIII\Http\Controllers\Json;
use Carbon\Carbon;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use FireflyIII\Services\Currency\ExchangeRateInterface;
use Illuminate\Http\Request;
use Log;
use Response;
/**
* Class ExchangeController
*
* @package FireflyIII\Http\Controllers\Json
*/
class ExchangeController extends Controller
{
/**
* @param Request $request
* @param TransactionCurrency $fromCurrency
* @param TransactionCurrency $toCurrency
* @param Carbon $date
*
* @return \Illuminate\Http\JsonResponse
*/
public function getRate(Request $request, TransactionCurrency $fromCurrency, TransactionCurrency $toCurrency, Carbon $date)
{
/** @var CurrencyRepositoryInterface $repository */
$repository = app(CurrencyRepositoryInterface::class);
$rate = $repository->getExchangeRate($fromCurrency, $toCurrency, $date);
$amount = null;
if (is_null($rate->id)) {
Log::debug(sprintf('No cached exchange rate in database for %s to %s on %s', $fromCurrency->code, $toCurrency->code, $date->format('Y-m-d')));
$preferred = env('EXCHANGE_RATE_SERVICE', config('firefly.preferred_exchange_service'));
$class = config('firefly.currency_exchange_services.' . $preferred);
/** @var ExchangeRateInterface $object */
$object = app($class);
$object->setUser(auth()->user());
$rate = $object->getRate($fromCurrency, $toCurrency, $date);
}
$return = $rate->toArray();
$return['amount'] = null;
if (!is_null($request->get('amount'))) {
// assume amount is in "from" currency:
$return['amount'] = bcmul($request->get('amount'), strval($rate->rate), 12);
// round to toCurrency decimal places:
$return['amount'] = round($return['amount'], $toCurrency->decimal_places);
}
return Response::json($return);
}
}

View File

@@ -10,6 +10,7 @@
*/
declare(strict_types=1);
namespace FireflyIII\Http\Controllers;
use Amount;
@@ -17,6 +18,7 @@ use Carbon\Carbon;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Collector\JournalCollectorInterface;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Account\AccountTaskerInterface;
use FireflyIII\Repositories\Bill\BillRepositoryInterface;
@@ -144,7 +146,7 @@ class JsonController extends Controller
* @return \Illuminate\Http\JsonResponse
*
*/
public function boxIn(AccountTaskerInterface $accountTasker, AccountRepositoryInterface $repository)
public function boxIn()
{
$start = session('start', Carbon::now()->startOfMonth());
$end = session('end', Carbon::now()->endOfMonth());
@@ -157,9 +159,15 @@ class JsonController extends Controller
if ($cache->has()) {
return Response::json($cache->get()); // @codeCoverageIgnore
}
$accounts = $repository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET, AccountType::CASH]);
$assets = $repository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]);
$amount = $accountTasker->amountInInPeriod($accounts, $assets, $start, $end);
// try a collector for income:
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($start, $end)
->setTypes([TransactionType::DEPOSIT])
->withOpposingAccount();
$amount = strval($collector->getJournals()->sum('transaction_amount'));
$data = ['box' => 'in', 'amount' => Amount::format($amount, false), 'amount_raw' => $amount];
$cache->store($data);
@@ -172,7 +180,7 @@ class JsonController extends Controller
*
* @return \Symfony\Component\HttpFoundation\Response
*/
public function boxOut(AccountTaskerInterface $accountTasker, AccountRepositoryInterface $repository)
public function boxOut()
{
$start = session('start', Carbon::now()->startOfMonth());
$end = session('end', Carbon::now()->endOfMonth());
@@ -186,9 +194,13 @@ class JsonController extends Controller
return Response::json($cache->get()); // @codeCoverageIgnore
}
$accounts = $repository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET, AccountType::CASH]);
$assets = $repository->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]);
$amount = $accountTasker->amountOutInPeriod($accounts, $assets, $start, $end);
// try a collector for expenses:
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
$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];
$cache->store($data);

View File

@@ -10,6 +10,7 @@
*/
declare(strict_types=1);
namespace FireflyIII\Http\Controllers;
use Carbon\Carbon;

View File

@@ -10,6 +10,7 @@
*/
declare(strict_types=1);
namespace FireflyIII\Http\Controllers;
use Amount;
@@ -209,10 +210,11 @@ class PiggyBankController extends Controller
$end = session('end', Carbon::now()->endOfMonth());
$accounts = [];
Log::debug('Looping piggues');
/** @var PiggyBank $piggyBank */
foreach ($piggyBanks as $piggyBank) {
$piggyBank->savedSoFar = $piggyBank->currentRelevantRep()->currentamount;
$piggyBank->percentage = $piggyBank->savedSoFar != 0 ? intval($piggyBank->savedSoFar / $piggyBank->targetamount * 100) : 0;
$piggyBank->savedSoFar = $piggyBank->currentRelevantRep()->currentamount ?? '0';
$piggyBank->percentage = bccomp('0', $piggyBank->savedSoFar) !== 0 ? intval($piggyBank->savedSoFar / $piggyBank->targetamount * 100) : 0;
$piggyBank->leftToSave = bcsub($piggyBank->targetamount, strval($piggyBank->savedSoFar));
$piggyBank->percentage = $piggyBank->percentage > 100 ? 100 : $piggyBank->percentage;
@@ -220,7 +222,9 @@ class PiggyBankController extends Controller
* Fill account information:
*/
$account = $piggyBank->account;
$new = false;
if (!isset($accounts[$account->id])) {
$new = true;
$accounts[$account->id] = [
'name' => $account->name,
'balance' => Steam::balanceIgnoreVirtual($account, $end),
@@ -230,7 +234,7 @@ class PiggyBankController extends Controller
'leftToSave' => $piggyBank->leftToSave,
];
}
if (isset($accounts[$account->id])) {
if (isset($accounts[$account->id]) && $new === false) {
$accounts[$account->id]['sumOfSaved'] = bcadd($accounts[$account->id]['sumOfSaved'], strval($piggyBank->savedSoFar));
$accounts[$account->id]['sumOfTargets'] = bcadd($accounts[$account->id]['sumOfTargets'], $piggyBank->targetamount);
$accounts[$account->id]['leftToSave'] = bcadd($accounts[$account->id]['leftToSave'], $piggyBank->leftToSave);
@@ -308,12 +312,7 @@ class PiggyBankController extends Controller
return redirect(route('piggy-banks.index'));
}
$amount = strval(round($request->get('amount'), 12));
$savedSoFar = $piggyBank->currentRelevantRep()->currentamount;
if (bccomp($amount, $savedSoFar) <= 0) {
}
Session::flash('error', strval(trans('firefly.cannot_remove_from_piggy', ['amount' => Amount::format($amount, false), 'name' => e($piggyBank->name)])));

View File

@@ -19,7 +19,6 @@ use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Collection\BalanceLine;
use FireflyIII\Helpers\Report\PopupReportInterface;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Repositories\Category\CategoryRepositoryInterface;

View File

@@ -134,6 +134,8 @@ class PreferencesController extends Controller
/**
* @param Request $request
*
* @param UserRepositoryInterface $repository
*
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function postIndex(Request $request, UserRepositoryInterface $repository)

View File

@@ -140,18 +140,18 @@ class ProfileController extends Controller
return redirect(route('index'));
}
/**
* @param User $user
* @param string $current
* @param string $new
* @param string $newConfirmation
*
* @return bool
* @throws ValidationException
*/
protected function validatePassword(User $user, string $current, string $new): bool
{
if (!Hash::check($current, auth()->user()->password)) {
if (!Hash::check($current, $user->password)) {
throw new ValidationException(strval(trans('firefly.invalid_current_password')));
}

View File

@@ -20,7 +20,6 @@ use FireflyIII\Models\Category;
use FireflyIII\Repositories\Category\CategoryRepositoryInterface;
use FireflyIII\Support\CacheProperties;
use Illuminate\Support\Collection;
use Log;
use Navigation;
/**

View File

@@ -19,6 +19,7 @@ use FireflyIII\Helpers\Collector\JournalCollectorInterface;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Account\AccountTaskerInterface;
use FireflyIII\Support\CacheProperties;
use Illuminate\Support\Collection;
@@ -31,13 +32,14 @@ class OperationsController extends Controller
{
/**
* @param AccountTaskerInterface $tasker
* @param Collection $accounts
* @param Carbon $start
* @param Carbon $end
*
* @return mixed|string
*/
public function expenses(Collection $accounts, Carbon $start, Carbon $end)
public function expenses(AccountTaskerInterface $tasker, Collection $accounts, Carbon $start, Carbon $end)
{
// chart properties for cache:
$cache = new CacheProperties;
@@ -48,7 +50,7 @@ class OperationsController extends Controller
if ($cache->has()) {
return $cache->get(); // @codeCoverageIgnore
}
$entries = $this->getExpenseReport($start, $end, $accounts);
$entries = $tasker->getExpenseReport($start, $end, $accounts);
$type = 'expense-entry';
$result = view('reports.partials.income-expenses', compact('entries', 'type'))->render();
$cache->store($result);
@@ -58,13 +60,14 @@ class OperationsController extends Controller
}
/**
* @param AccountTaskerInterface $tasker
* @param Collection $accounts
* @param Carbon $start
* @param Carbon $end
*
* @return string
*/
public function income(Collection $accounts, Carbon $start, Carbon $end)
public function income(AccountTaskerInterface $tasker, Collection $accounts, Carbon $start, Carbon $end)
{
// chart properties for cache:
$cache = new CacheProperties;
@@ -75,7 +78,7 @@ class OperationsController extends Controller
if ($cache->has()) {
return $cache->get(); // @codeCoverageIgnore
}
$entries = $this->getIncomeReport($start, $end, $accounts);
$entries = $tasker->getIncomeReport($start, $end, $accounts);
$type = 'income-entry';
$result = view('reports.partials.income-expenses', compact('entries', 'type'))->render();
@@ -86,13 +89,14 @@ class OperationsController extends Controller
}
/**
* @param AccountTaskerInterface $tasker
* @param Collection $accounts
* @param Carbon $start
* @param Carbon $end
*
* @return mixed|string
*/
public function operations(Collection $accounts, Carbon $start, Carbon $end)
public function operations(AccountTaskerInterface $tasker, Collection $accounts, Carbon $start, Carbon $end)
{
// chart properties for cache:
$cache = new CacheProperties;
@@ -104,8 +108,8 @@ class OperationsController extends Controller
return $cache->get(); // @codeCoverageIgnore
}
$incomes = $this->getIncomeReport($start, $end, $accounts);
$expenses = $this->getExpenseReport($start, $end, $accounts);
$incomes = $tasker->getIncomeReport($start, $end, $accounts);
$expenses = $tasker->getExpenseReport($start, $end, $accounts);
$incomeSum = array_sum(
array_map(
function ($item) {
@@ -129,125 +133,4 @@ class OperationsController extends Controller
}
/**
* @param Carbon $start
* @param Carbon $end
* @param Collection $accounts
*
* @return array
*/
private function getExpenseReport(Carbon $start, Carbon $end, Collection $accounts): array
{
// get all expenses for the given accounts in the given period!
// also transfers!
// get all transactions:
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
$collector->setAccounts($accounts)->setRange($start, $end);
$collector->setTypes([TransactionType::WITHDRAWAL, TransactionType::TRANSFER])
->withOpposingAccount()
->enableInternalFilter();
$transactions = $collector->getJournals();
$transactions = $transactions->filter(
function (Transaction $transaction) {
// return negative amounts only.
if (bccomp($transaction->transaction_amount, '0') === -1) {
return $transaction;
}
return false;
}
);
$expenses = $this->groupByOpposing($transactions);
// sort the result
// Obtain a list of columns
$sum = [];
foreach ($expenses as $accountId => $row) {
$sum[$accountId] = floatval($row['sum']);
}
array_multisort($sum, SORT_ASC, $expenses);
return $expenses;
}
/**
* @param Carbon $start
* @param Carbon $end
* @param Collection $accounts
*
* @return array
*/
private function getIncomeReport(Carbon $start, Carbon $end, Collection $accounts): array
{
// get all expenses for the given accounts in the given period!
// also transfers!
// get all transactions:
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
$collector->setAccounts($accounts)->setRange($start, $end);
$collector->setTypes([TransactionType::DEPOSIT, TransactionType::TRANSFER])
->withOpposingAccount()
->enableInternalFilter();
$transactions = $collector->getJournals();
$transactions = $transactions->filter(
function (Transaction $transaction) {
// return positive amounts only.
if (bccomp($transaction->transaction_amount, '0') === 1) {
return $transaction;
}
return false;
}
);
$income = $this->groupByOpposing($transactions);
// sort the result
// Obtain a list of columns
$sum = [];
foreach ($income as $accountId => $row) {
$sum[$accountId] = floatval($row['sum']);
}
array_multisort($sum, SORT_DESC, $income);
return $income;
}
/**
* @param Collection $transactions
*
* @return array
*/
private function groupByOpposing(Collection $transactions): array
{
$expenses = [];
// join the result together:
foreach ($transactions as $transaction) {
$opposingId = $transaction->opposing_account_id;
$name = $transaction->opposing_account_name;
if (!isset($expenses[$opposingId])) {
$expenses[$opposingId] = [
'id' => $opposingId,
'name' => $name,
'sum' => '0',
'average' => '0',
'count' => 0,
];
}
$expenses[$opposingId]['sum'] = bcadd($expenses[$opposingId]['sum'], $transaction->transaction_amount);
$expenses[$opposingId]['count']++;
}
// do averages:
foreach ($expenses as $key => $entry) {
if ($expenses[$key]['count'] > 1) {
$expenses[$key]['average'] = bcdiv($expenses[$key]['sum'], strval($expenses[$key]['count']));
}
}
return $expenses;
}
}

View File

@@ -262,6 +262,7 @@ class ReportController extends Controller
$categories = join(',', $request->getCategoryList()->pluck('id')->toArray());
$budgets = join(',', $request->getBudgetList()->pluck('id')->toArray());
$tags = join(',', $request->getTagList()->pluck('tag')->toArray());
$uri = route('reports.index');
if ($request->getAccountList()->count() === 0) {
Log::debug('Account count is zero');

View File

@@ -142,6 +142,8 @@ class TagController extends Controller
/**
* @param Tag $tag
*
* @param TagRepositoryInterface $repository
*
* @return View
*/
public function edit(Tag $tag, TagRepositoryInterface $repository)
@@ -241,6 +243,8 @@ class TagController extends Controller
$start = null;
$end = null;
$periods = new Collection;
$apiKey = env('GOOGLE_MAPS_API_KEY', '');
$sum = '0';
// prep for "all" view.
@@ -248,6 +252,7 @@ class TagController extends Controller
$subTitle = trans('firefly.all_journals_for_tag', ['tag' => $tag->tag]);
$start = $repository->firstUseDate($tag);
$end = new Carbon;
$sum = $repository->sumOfTag($tag);
}
// prep for "specific date" view.
@@ -260,6 +265,7 @@ class TagController extends Controller
'start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)]
);
$periods = $this->getPeriodOverview($tag);
$sum = $repository->sumOfTag($tag, $start, $end);
}
// prep for current period
@@ -298,9 +304,8 @@ class TagController extends Controller
['tag' => $tag->tag, 'start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)]
);
}
$sum = '0';
return view('tags.show', compact('tag', 'periods', 'subTitle', 'subTitleIcon', 'journals', 'sum', 'start', 'end', 'moment'));
return view('tags.show', compact('apiKey', 'tag', 'periods', 'subTitle', 'subTitleIcon', 'journals', 'sum', 'start', 'end', 'moment'));
}

View File

@@ -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,22 @@ class SingleController extends Controller
'invoice_date' => $journal->dateAsString('invoice_date'),
'interal_reference' => $journal->getMeta('internal_reference'),
'notes' => $journal->getMeta('notes'),
// exchange rate fields
'native_amount' => $journal->amountPositive(),
'native_currency' => $journal->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
}
if ($journal->isWithdrawal() && $destinationAccounts->first()->accountType->type == AccountType::CASH) {
$preFilled['destination_account_name'] = '';
}

View File

@@ -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;
@@ -60,6 +61,8 @@ class TransactionController extends Controller
* @param JournalRepositoryInterface $repository
* @param string $what
*
* @param string $moment
*
* @return View
*/
public function index(Request $request, JournalRepositoryInterface $repository, string $what, string $moment = '')
@@ -179,8 +182,17 @@ 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;
return view('transactions.show', compact('journal', 'events', 'subTitle', 'what', 'transactions'));
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'));
}

View File

@@ -43,14 +43,12 @@ class AccountFormRequest extends Request
'accountType' => $this->string('what'),
'currency_id' => $this->integer('currency_id'),
'virtualBalance' => $this->float('virtualBalance'),
'virtualBalanceCurrency' => $this->integer('amount_currency_id_virtualBalance'),
'iban' => $this->string('iban'),
'BIC' => $this->string('BIC'),
'accountNumber' => $this->string('accountNumber'),
'accountRole' => $this->string('accountRole'),
'openingBalance' => $this->float('openingBalance'),
'openingBalanceDate' => $this->date('openingBalanceDate'),
'openingBalanceCurrency' => $this->integer('amount_currency_id_openingBalance'),
'ccType' => $this->string('ccType'),
'ccMonthlyPaymentDate' => $this->string('ccMonthlyPaymentDate'),
];

View File

@@ -67,6 +67,11 @@ class JournalFormRequest extends Request
'destination_account_name' => $this->string('destination_account_name'),
'piggy_bank_id' => $this->integer('piggy_bank_id'),
// native amount and stuff like that:
'native_amount' => $this->float('native_amount'),
'source_amount' => $this->float('source_amount'),
'destination_amount' => $this->float('destination_amount'),
];
return $data;
@@ -101,6 +106,11 @@ class JournalFormRequest extends Request
'destination_account_id' => 'numeric|belongsToUser:accounts,id',
'destination_account_name' => 'between:1,255',
'piggy_bank_id' => 'between:1,255',
// foreign currency amounts
'native_amount' => 'numeric|more:0',
'source_amount' => 'numeric|more:0',
'destination_amount' => 'numeric|more:0',
];
// some rules get an upgrade depending on the type of data:

View File

@@ -10,6 +10,7 @@
*/
declare(strict_types=1);
namespace FireflyIII\Http\Requests;
use FireflyIII\Repositories\Tag\TagRepositoryInterface;

View File

@@ -77,13 +77,13 @@ Breadcrumbs::register(
if ($moment === 'all') {
$breadcrumbs->push(trans('firefly.everything'), route('accounts.show', [$account->id, 'all']));
}
// when is specific period:
if (strlen($moment) > 0 && $moment !== 'all') {
// when is specific period or when empty:
if ($moment !== 'all') {
$title = trans(
'firefly.between_dates_breadcrumb', ['start' => $start->formatLocalized(strval(trans('config.month_and_day'))),
'end' => $end->formatLocalized(strval(trans('config.month_and_day')))]
);
$breadcrumbs->push($title, route('accounts.show', [$account->id, $moment]));
$breadcrumbs->push($title, route('accounts.show', [$account->id, $moment, $start, $end]));
}
}
@@ -258,7 +258,7 @@ Breadcrumbs::register(
$breadcrumbs->push(trans('firefly.everything'), route('budgets.no-budget', ['all']));
}
// when is specific period:
if (strlen($moment) > 0 && $moment !== 'all') {
if ($moment !== 'all') {
$title = trans(
'firefly.between_dates_breadcrumb', ['start' => $start->formatLocalized(strval(trans('config.month_and_day'))),
'end' => $end->formatLocalized(strval(trans('config.month_and_day')))]
@@ -274,6 +274,7 @@ Breadcrumbs::register(
'budgets.show', function (BreadCrumbGenerator $breadcrumbs, Budget $budget) {
$breadcrumbs->parent('budgets.index');
$breadcrumbs->push(e($budget->name), route('budgets.show', [$budget->id]));
$breadcrumbs->push(trans('firefly.everything'), route('budgets.show', [$budget->id]));
}
);
@@ -333,7 +334,7 @@ Breadcrumbs::register(
$breadcrumbs->push(trans('firefly.everything'), route('categories.show', [$category->id, 'all']));
}
// when is specific period:
if (strlen($moment) > 0 && $moment !== 'all') {
if ($moment !== 'all') {
$title = trans(
'firefly.between_dates_breadcrumb', ['start' => $start->formatLocalized(strval(trans('config.month_and_day'))),
'end' => $end->formatLocalized(strval(trans('config.month_and_day')))]
@@ -354,7 +355,7 @@ Breadcrumbs::register(
$breadcrumbs->push(trans('firefly.everything'), route('categories.no-category', ['all']));
}
// when is specific period:
if (strlen($moment) > 0 && $moment !== 'all') {
if ($moment !== 'all') {
$title = trans(
'firefly.between_dates_breadcrumb', ['start' => $start->formatLocalized(strval(trans('config.month_and_day'))),
'end' => $end->formatLocalized(strval(trans('config.month_and_day')))]
@@ -709,23 +710,33 @@ Breadcrumbs::register(
Breadcrumbs::register(
'tags.edit', function (BreadCrumbGenerator $breadcrumbs, Tag $tag) {
$breadcrumbs->parent('tags.show', $tag);
$breadcrumbs->parent('tags.show', $tag, '', new Carbon, new Carbon);
$breadcrumbs->push(trans('breadcrumbs.edit_tag', ['tag' => e($tag->tag)]), route('tags.edit', [$tag->id]));
}
);
Breadcrumbs::register(
'tags.delete', function (BreadCrumbGenerator $breadcrumbs, Tag $tag) {
$breadcrumbs->parent('tags.show', $tag);
$breadcrumbs->parent('tags.show', $tag, '', new Carbon, new Carbon);
$breadcrumbs->push(trans('breadcrumbs.delete_tag', ['tag' => e($tag->tag)]), route('tags.delete', [$tag->id]));
}
);
Breadcrumbs::register(
'tags.show', function (BreadCrumbGenerator $breadcrumbs, Tag $tag) {
'tags.show', function (BreadCrumbGenerator $breadcrumbs, Tag $tag, string $moment, Carbon $start, Carbon $end) {
$breadcrumbs->parent('tags.index');
$breadcrumbs->push(e($tag->tag), route('tags.show', [$tag->id]));
$breadcrumbs->push(e($tag->tag), route('tags.show', [$tag->id], $moment));
if ($moment === 'all') {
$breadcrumbs->push(trans('firefly.everything'), route('tags.show', [$tag->id], $moment));
}
if ($moment !== 'all') {
$title = trans(
'firefly.between_dates_breadcrumb', ['start' => $start->formatLocalized(strval(trans('config.month_and_day'))),
'end' => $end->formatLocalized(strval(trans('config.month_and_day')))]
);
$breadcrumbs->push($title, route('tags.show', [$tag->id], $moment));
}
}
);
@@ -743,7 +754,7 @@ Breadcrumbs::register(
}
// when is specific period:
if (strlen($moment) > 0 && $moment !== 'all') {
if ($moment !== 'all') {
$title = trans(
'firefly.between_dates_breadcrumb', ['start' => $start->formatLocalized(strval(trans('config.month_and_day'))),
'end' => $end->formatLocalized(strval(trans('config.month_and_day')))]

View File

@@ -211,6 +211,26 @@ class Account extends Model
return $value;
}
/**
* Returns the opening balance
*
* @return TransactionJournal
* @throws FireflyException
*/
public function getOpeningBalance(): TransactionJournal
{
$journal = TransactionJournal::sortCorrectly()
->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id')
->where('transactions.account_id', $this->id)
->transactionTypes([TransactionType::OPENING_BALANCE])
->first(['transaction_journals.*']);
if (is_null($journal)) {
return new TransactionJournal;
}
return $journal;
}
/**
* Returns the amount of the opening balance for this account.
*

View File

@@ -0,0 +1,53 @@
<?php
/**
* CurrencyExchange.php
* 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.
*/
declare(strict_types=1);
namespace FireflyIII\Models;
use FireflyIII\User;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* Class CurrencyExchange
*
* @package FireflyIII\Models
*/
class CurrencyExchangeRate extends Model
{
protected $dates = ['created_at', 'updated_at', 'date'];
/**
* @return BelongsTo
*/
public function fromCurrency(): BelongsTo
{
return $this->belongsTo(TransactionCurrency::class, 'from_currency_id');
}
/**
* @return BelongsTo
*/
public function toCurrency(): BelongsTo
{
return $this->belongsTo(TransactionCurrency::class, 'to_currency_id');
}
/**
* @return BelongsTo
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View File

@@ -16,6 +16,7 @@ namespace FireflyIII\Models;
use Carbon\Carbon;
use Crypt;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
use Steam;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
@@ -67,7 +68,7 @@ class PiggyBank extends Model
/**
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function account()
public function account(): BelongsTo
{
return $this->belongsTo('FireflyIII\Models\Account');
}

View File

@@ -10,6 +10,7 @@
*/
declare(strict_types=1);
namespace FireflyIII\Models;

View File

@@ -43,6 +43,7 @@ use FireflyIII\Support\FireflyConfig;
use FireflyIII\Support\Navigation;
use FireflyIII\Support\Preferences;
use FireflyIII\Support\Steam;
use FireflyIII\Support\Twig\Account;
use FireflyIII\Support\Twig\General;
use FireflyIII\Support\Twig\Journal;
use FireflyIII\Support\Twig\PiggyBank;
@@ -77,6 +78,7 @@ class FireflyServiceProvider extends ServiceProvider
Twig::addExtension(new Translation);
Twig::addExtension(new Transaction);
Twig::addExtension(new Rule);
Twig::addExtension(new Account);
}
/**

View File

@@ -455,6 +455,7 @@ class AccountRepository implements AccountRepositoryInterface
{
$amount = $data['openingBalance'];
$name = $data['name'];
$currencyId = $data['currency_id'];
$opposing = $this->storeOpposingAccount($name);
$transactionType = TransactionType::whereType(TransactionType::OPENING_BALANCE)->first();
/** @var TransactionJournal $journal */
@@ -462,7 +463,7 @@ class AccountRepository implements AccountRepositoryInterface
[
'user_id' => $this->user->id,
'transaction_type_id' => $transactionType->id,
'transaction_currency_id' => $data['openingBalanceCurrency'],
'transaction_currency_id' => $currencyId,
'description' => 'Initial balance for "' . $account->name . '"',
'completed' => true,
'date' => $data['openingBalanceDate'],
@@ -530,12 +531,8 @@ class AccountRepository implements AccountRepositoryInterface
}
// opening balance data? update it!
if (!is_null($openingBalance->id)) {
$date = $data['openingBalanceDate'];
$amount = $data['openingBalance'];
Log::debug('Opening balance journal found, update journal.');
$this->updateOpeningBalanceJournal($account, $openingBalance, $date, $amount);
$this->updateOpeningBalanceJournal($account, $openingBalance, $data);
return true;
}
@@ -589,15 +586,19 @@ class AccountRepository implements AccountRepositoryInterface
/**
* @param Account $account
* @param TransactionJournal $journal
* @param Carbon $date
* @param float $amount
* @param array $data
*
* @return bool
*/
protected function updateOpeningBalanceJournal(Account $account, TransactionJournal $journal, Carbon $date, float $amount): bool
protected function updateOpeningBalanceJournal(Account $account, TransactionJournal $journal, array $data): bool
{
$date = $data['openingBalanceDate'];
$amount = $data['openingBalance'];
$currencyId = intval($data['currency_id']);
// update date:
$journal->date = $date;
$journal->transaction_currency_id = $currencyId;
$journal->save();
// update transactions:
/** @var Transaction $transaction */

View File

@@ -137,4 +137,12 @@ interface AccountRepositoryInterface
*/
public function store(array $data): Account;
/**
* @param Account $account
* @param array $data
*
* @return Account
*/
public function update(Account $account, array $data): Account;
}

View File

@@ -14,9 +14,10 @@ declare(strict_types = 1);
namespace FireflyIII\Repositories\Account;
use Carbon\Carbon;
use FireflyIII\Helpers\Collector\JournalCollectorInterface;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionType;
use FireflyIII\User;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Collection;
use Log;
use Steam;
@@ -31,64 +32,6 @@ class AccountTasker implements AccountTaskerInterface
/** @var User */
private $user;
/**
* @see self::amountInPeriod
*
* @param Collection $accounts
* @param Collection $excluded
* @param Carbon $start
* @param Carbon $end
*
* @return string
*/
public function amountInInPeriod(Collection $accounts, Collection $excluded, Carbon $start, Carbon $end): string
{
$idList = [
'accounts' => $accounts->pluck('id')->toArray(),
'exclude' => $excluded->pluck('id')->toArray(),
];
Log::debug(
'Now calling amountInInPeriod.',
['accounts' => $idList['accounts'], 'excluded' => $idList['exclude'],
'start' => $start->format('Y-m-d'),
'end' => $end->format('Y-m-d'),
]
);
return $this->amountInPeriod($idList, $start, $end, true);
}
/**
* @see self::amountInPeriod
*
* @param Collection $accounts
* @param Collection $excluded
* @param Carbon $start
* @param Carbon $end
*
* @return string
*/
public function amountOutInPeriod(Collection $accounts, Collection $excluded, Carbon $start, Carbon $end): string
{
$idList = [
'accounts' => $accounts->pluck('id')->toArray(),
'exclude' => $excluded->pluck('id')->toArray(),
];
Log::debug(
'Now calling amountOutInPeriod.',
['accounts' => $idList['accounts'], 'excluded' => $idList['exclude'],
'start' => $start->format('Y-m-d'),
'end' => $end->format('Y-m-d'),
]
);
return $this->amountInPeriod($idList, $start, $end, false);
}
/**
* @param Collection $accounts
* @param Carbon $start
@@ -147,6 +90,92 @@ class AccountTasker implements AccountTaskerInterface
return $return;
}
/**
* @param Carbon $start
* @param Carbon $end
* @param Collection $accounts
*
* @return array
*/
public function getExpenseReport(Carbon $start, Carbon $end, Collection $accounts): array
{
// get all expenses for the given accounts in the given period!
// also transfers!
// get all transactions:
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
$collector->setAccounts($accounts)->setRange($start, $end);
$collector->setTypes([TransactionType::WITHDRAWAL, TransactionType::TRANSFER])
->withOpposingAccount()
->enableInternalFilter();
$transactions = $collector->getJournals();
$transactions = $transactions->filter(
function (Transaction $transaction) {
// return negative amounts only.
if (bccomp($transaction->transaction_amount, '0') === -1) {
return $transaction;
}
return false;
}
);
$expenses = $this->groupByOpposing($transactions);
// sort the result
// Obtain a list of columns
$sum = [];
foreach ($expenses as $accountId => $row) {
$sum[$accountId] = floatval($row['sum']);
}
array_multisort($sum, SORT_ASC, $expenses);
return $expenses;
}
/**
* @param Carbon $start
* @param Carbon $end
* @param Collection $accounts
*
* @return array
*/
public function getIncomeReport(Carbon $start, Carbon $end, Collection $accounts): array
{
// get all expenses for the given accounts in the given period!
// also transfers!
// get all transactions:
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
$collector->setAccounts($accounts)->setRange($start, $end);
$collector->setTypes([TransactionType::DEPOSIT, TransactionType::TRANSFER])
->withOpposingAccount()
->enableInternalFilter();
$transactions = $collector->getJournals();
$transactions = $transactions->filter(
function (Transaction $transaction) {
// return positive amounts only.
if (bccomp($transaction->transaction_amount, '0') === 1) {
return $transaction;
}
return false;
}
);
$income = $this->groupByOpposing($transactions);
// sort the result
// Obtain a list of columns
$sum = [];
foreach ($income as $accountId => $row) {
$sum[$accountId] = floatval($row['sum']);
}
array_multisort($sum, SORT_DESC, $income);
return $income;
}
/**
* @param User $user
*/
@@ -156,62 +185,37 @@ class AccountTasker implements AccountTaskerInterface
}
/**
* Will return how much money has been going out (ie. spent) by the given account(s).
* Alternatively, will return how much money has been coming in (ie. earned) by the given accounts.
* @param Collection $transactions
*
* Enter $incoming=true for any money coming in (income)
* Enter $incoming=false for any money going out (expenses)
*
* This means any money going out or in. You can also submit accounts to exclude,
* so transfers between accounts are not included.
*
* As a general rule:
*
* - Asset accounts should return both expenses and earnings. But could return 0.
* - Expense accounts (where money is spent) should only return earnings (the account gets money).
* - Revenue accounts (where money comes from) should only return expenses (they spend money).
*
*
*
* @param array $accounts
* @param Carbon $start
* @param Carbon $end
* @param bool $incoming
*
* @return string
* @return array
*/
protected function amountInPeriod(array $accounts, Carbon $start, Carbon $end, bool $incoming): string
private function groupByOpposing(Collection $transactions): array
{
$joinModifier = $incoming ? '<' : '>';
$selection = $incoming ? '>' : '<';
$query = Transaction::distinct()
->leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->leftJoin(
'transactions as other_side', function (JoinClause $join) use ($joinModifier) {
$join->on('transaction_journals.id', '=', 'other_side.transaction_journal_id')->where('other_side.amount', $joinModifier, 0);
$expenses = [];
// join the result together:
foreach ($transactions as $transaction) {
$opposingId = $transaction->opposing_account_id;
$name = $transaction->opposing_account_name;
if (!isset($expenses[$opposingId])) {
$expenses[$opposingId] = [
'id' => $opposingId,
'name' => $name,
'sum' => '0',
'average' => '0',
'count' => 0,
];
}
$expenses[$opposingId]['sum'] = bcadd($expenses[$opposingId]['sum'], $transaction->transaction_amount);
$expenses[$opposingId]['count']++;
}
// do averages:
foreach ($expenses as $key => $entry) {
if ($expenses[$key]['count'] > 1) {
$expenses[$key]['average'] = bcdiv($expenses[$key]['sum'], strval($expenses[$key]['count']));
}
)
->where('transaction_journals.date', '>=', $start->format('Y-m-d'))
->where('transaction_journals.date', '<=', $end->format('Y-m-d'))
->where('transaction_journals.user_id', $this->user->id)
->whereNull('transactions.deleted_at')
->whereNull('transaction_journals.deleted_at')
->whereIn('transactions.account_id', $accounts['accounts'])
->where('transactions.amount', $selection, 0);
if (count($accounts['exclude']) > 0) {
$query->whereNotIn('other_side.account_id', $accounts['exclude']);
}
$result = $query->get(['transactions.id', 'transactions.amount']);
$sum = strval($result->sum('amount'));
if (strlen($sum) === 0) {
Log::debug('Sum is empty.');
$sum = '0';
}
Log::debug(sprintf('Result is %s', $sum));
return $sum;
return $expenses;
}
}

View File

@@ -24,30 +24,6 @@ use Illuminate\Support\Collection;
*/
interface AccountTaskerInterface
{
/**
* @param Collection $accounts
* @param Collection $excluded
* @param Carbon $start
* @param Carbon $end
*
* @see AccountTasker::amountInPeriod()
*
* @return string
*/
public function amountInInPeriod(Collection $accounts, Collection $excluded, Carbon $start, Carbon $end): string;
/**
* @param Collection $accounts
* @param Collection $excluded
* @param Carbon $start
* @param Carbon $end
*
* @see AccountTasker::amountInPeriod()
*
* @return string
*/
public function amountOutInPeriod(Collection $accounts, Collection $excluded, Carbon $start, Carbon $end): string;
/**
* @param Collection $accounts
* @param Carbon $start
@@ -57,6 +33,24 @@ interface AccountTaskerInterface
*/
public function getAccountReport(Collection $accounts, Carbon $start, Carbon $end): array;
/**
* @param Carbon $start
* @param Carbon $end
* @param Collection $accounts
*
* @return array
*/
public function getExpenseReport(Carbon $start, Carbon $end, Collection $accounts): array;
/**
* @param Carbon $start
* @param Carbon $end
* @param Collection $accounts
*
* @return array
*/
public function getIncomeReport(Carbon $start, Carbon $end, Collection $accounts): array;
/**
* @param User $user
*/

View File

@@ -14,10 +14,13 @@ declare(strict_types = 1);
namespace FireflyIII\Repositories\Currency;
use Carbon\Carbon;
use FireflyIII\Models\CurrencyExchangeRate;
use FireflyIII\Models\Preference;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\User;
use Illuminate\Support\Collection;
use Log;
use Preferences;
/**
@@ -178,6 +181,38 @@ class CurrencyRepository implements CurrencyRepositoryInterface
return $preferred;
}
/**
* @param TransactionCurrency $fromCurrency
* @param TransactionCurrency $toCurrency
* @param Carbon $date
*
* @return CurrencyExchangeRate
*/
public function getExchangeRate(TransactionCurrency $fromCurrency, TransactionCurrency $toCurrency, Carbon $date): CurrencyExchangeRate
{
if ($fromCurrency->id === $toCurrency->id) {
$rate = new CurrencyExchangeRate;
$rate->rate = 1;
$rate->id = 0;
return $rate;
}
$rate = $this->user->currencyExchangeRates()
->where('from_currency_id', $fromCurrency->id)
->where('to_currency_id', $toCurrency->id)
->where('date', $date->format('Y-m-d'))->first();
if (!is_null($rate)) {
Log::debug(sprintf('Found cached exchange rate in database for %s to %s on %s', $fromCurrency->code, $toCurrency->code, $date->format('Y-m-d')));
return $rate;
}
return new CurrencyExchangeRate;
}
/**
* @param User $user
*/

View File

@@ -14,6 +14,8 @@ declare(strict_types = 1);
namespace FireflyIII\Repositories\Currency;
use Carbon\Carbon;
use FireflyIII\Models\CurrencyExchangeRate;
use FireflyIII\Models\Preference;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\User;
@@ -95,6 +97,15 @@ interface CurrencyRepositoryInterface
*/
public function getCurrencyByPreference(Preference $preference): TransactionCurrency;
/**
* @param TransactionCurrency $fromCurrency
* @param TransactionCurrency $toCurrency
* @param Carbon $date
*
* @return CurrencyExchangeRate
*/
public function getExchangeRate(TransactionCurrency $fromCurrency, TransactionCurrency $toCurrency, Carbon $date): CurrencyExchangeRate;
/**
* @param User $user
*/

View File

@@ -41,7 +41,10 @@ class JournalRepository implements JournalRepositoryInterface
private $user;
/** @var array */
private $validMetaFields = ['interest_date', 'book_date', 'process_date', 'due_date', 'payment_date', 'invoice_date', 'internal_reference', 'notes'];
private $validMetaFields
= ['interest_date', 'book_date', 'process_date', 'due_date', 'payment_date', 'invoice_date', 'internal_reference', 'notes', 'foreign_amount',
'foreign_currency_id',
];
/**
* @param TransactionJournal $journal
@@ -165,12 +168,17 @@ class JournalRepository implements JournalRepositoryInterface
public function store(array $data): TransactionJournal
{
// find transaction type.
/** @var TransactionType $transactionType */
$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' => $data['currency_id'],
'transaction_currency_id' => $currencyId,
'description' => $data['description'],
'completed' => 0,
'date' => $data['date'],
@@ -181,13 +189,13 @@ class JournalRepository implements JournalRepositoryInterface
// store stuff:
$this->storeCategoryWithJournal($journal, $data['category']);
$this->storeBudgetWithJournal($journal, $data['budget_id']);
$accounts = $this->storeAccounts($transactionType, $data);
// store two transactions:
$one = [
'journal' => $journal,
'account' => $accounts['source'],
'amount' => bcmul(strval($data['amount']), '-1'),
'amount' => bcmul($amount, '-1'),
'description' => null,
'category' => null,
'budget' => null,
@@ -198,7 +206,7 @@ class JournalRepository implements JournalRepositoryInterface
$two = [
'journal' => $journal,
'account' => $accounts['destination'],
'amount' => $data['amount'],
'amount' => $amount,
'description' => null,
'category' => null,
'budget' => null,
@@ -236,10 +244,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'];
$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();
@@ -247,12 +267,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();
@@ -745,4 +762,56 @@ class JournalRepository implements JournalRepositoryInterface
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.
*
* @param array $data
* @param array $accounts
*
* @return array
* @throws FireflyException
*/
private function verifyNativeAmount(array $data, array $accounts): array
{
/** @var TransactionType $transactionType */
$transactionType = TransactionType::where('type', ucfirst($data['what']))->first();
$submittedCurrencyId = $data['currency_id'];
// which account to check for what the native currency is?
$check = 'source';
if ($transactionType->type === TransactionType::DEPOSIT) {
$check = 'destination';
}
switch ($transactionType->type) {
case TransactionType::DEPOSIT:
case TransactionType::WITHDRAWAL:
// continue:
$nativeCurrencyId = intval($accounts[$check]->getMeta('currency_id'));
// does not match? Then user has submitted amount in a foreign currency:
if ($nativeCurrencyId !== $submittedCurrencyId) {
// store amount and submitted currency in "foreign currency" fields:
$data['foreign_amount'] = $data['amount'];
$data['foreign_currency_id'] = $submittedCurrencyId;
// overrule the amount and currency ID fields to be the original again:
$data['amount'] = strval($data['native_amount']);
$data['currency_id'] = $nativeCurrencyId;
}
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'));
break;
default:
throw new FireflyException(sprintf('Cannot handle %s in verifyNativeAmount()', $transactionType->type));
}
return $data;
}
}

View File

@@ -246,6 +246,28 @@ class TagRepository implements TagRepositoryInterface
}
/**
* @param Tag $tag
* @param Carbon|null $start
* @param Carbon|null $end
*
* @return string
*/
public function sumOfTag(Tag $tag, Carbon $start = null, Carbon $end = null): string
{
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
if (!is_null($start) && !is_null($end)) {
$collector->setRange($start, $end);
}
$collector->setAllAssetAccounts()->setTag($tag);
$sum = $collector->getJournals()->sum('transaction_amount');
return strval($sum);
}
/**
* Can a tag become an advance payment?
*

View File

@@ -126,6 +126,15 @@ interface TagRepositoryInterface
*/
public function store(array $data): Tag;
/**
* @param Tag $tag
* @param Carbon|null $start
* @param Carbon|null $end
*
* @return string
*/
public function sumOfTag(Tag $tag, Carbon $start = null, Carbon $end = null): string;
/**
* @param Tag $tag
*

View File

@@ -0,0 +1,61 @@
<?php
/**
* HasAttachment.php
* 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.
*/
declare(strict_types=1);
namespace FireflyIII\Rules\Triggers;
use FireflyIII\Models\TransactionJournal;
class HasAttachment extends AbstractTrigger implements TriggerInterface
{
/**
* A trigger is said to "match anything", or match any given transaction,
* when the trigger value is very vague or has no restrictions. Easy examples
* are the "AmountMore"-trigger combined with an amount of 0: any given transaction
* has an amount of more than zero! Other examples are all the "Description"-triggers
* which have hard time handling empty trigger values such as "" or "*" (wild cards).
*
* If the user tries to create such a trigger, this method MUST return true so Firefly III
* can stop the storing / updating the trigger. If the trigger is in any way restrictive
* (even if it will still include 99.9% of the users transactions), this method MUST return
* false.
*
* @param null $value
*
* @return bool
*/
public static function willMatchEverything($value = null)
{
$value = intval($value);
if ($value < 0) {
return true;
}
return false;
}
/**
* @param TransactionJournal $journal
*
* @return bool
*/
public function triggered(TransactionJournal $journal): bool
{
$minimum = intval($this->triggerValue);
$attachments = $journal->attachments()->count();
if ($attachments >= $minimum) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,38 @@
<?php
/**
* ExchangeRateInterface.php
* 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.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Currency;
use Carbon\Carbon;
use FireflyIII\Models\CurrencyExchangeRate;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\User;
interface ExchangeRateInterface
{
/**
* @param TransactionCurrency $fromCurrency
* @param TransactionCurrency $toCurrency
* @param Carbon $date
*
* @return CurrencyExchangeRate
*/
public function getRate(TransactionCurrency $fromCurrency, TransactionCurrency $toCurrency, Carbon $date): CurrencyExchangeRate;
/**
* @param User $user
*
* @return mixed
*/
public function setUser(User $user);
}

View File

@@ -0,0 +1,69 @@
<?php
/**
* FixerIO.php
* 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.
*/
declare(strict_types=1);
namespace FireflyIII\Services\Currency;
use Carbon\Carbon;
use FireflyIII\Models\CurrencyExchangeRate;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\User;
use Log;
use Requests;
/**
* Class FixerIO
*
* @package FireflyIII\Services\Currency
*/
class FixerIO implements ExchangeRateInterface
{
/** @var User */
protected $user;
public function getRate(TransactionCurrency $fromCurrency, TransactionCurrency $toCurrency, Carbon $date): CurrencyExchangeRate
{
$uri = sprintf('https://api.fixer.io/%s?base=%s&symbols=%s', $date->format('Y-m-d'), $fromCurrency->code, $toCurrency->code);
$result = Requests::get($uri);
$rate = 1.0;
$content = null;
if ($result->status_code !== 200) {
Log::error(sprintf('Something went wrong. Received error code %d and body "%s" from FixerIO.', $result->status_code, $result->body));
}
// get rate from body:
if ($result->status_code === 200) {
$content = json_decode($result->body, true);
}
if (!is_null($content)) {
$code = $toCurrency->code;
$rate = isset($content['rates'][$code]) ? $content['rates'][$code] : '1';
}
// create new currency exchange rate object:
$exchangeRate = new CurrencyExchangeRate;
$exchangeRate->user()->associate($this->user);
$exchangeRate->fromCurrency()->associate($fromCurrency);
$exchangeRate->toCurrency()->associate($toCurrency);
$exchangeRate->date = $date;
$exchangeRate->rate = $rate;
$exchangeRate->save();
return $exchangeRate;
}
/**
* @param User $user
*/
public function setUser(User $user)
{
$this->user = $user;
}
}

View File

@@ -0,0 +1,39 @@
<?php
/**
* CurrencyCode.php
* 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.
*/
declare(strict_types=1);
namespace FireflyIII\Support\Binder;
use FireflyIII\Models\TransactionCurrency;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
/**
* Class CurrencyCode
*
* @package FireflyIII\Support\Binder
*/
class CurrencyCode implements BinderInterface
{
/**
* @param $value
* @param $route
*
* @return mixed
*/
public static function routeBinder($value, $route)
{
$currency = TransactionCurrency::where('code', $value)->first();
if (!is_null($currency)) {
return $currency;
}
throw new NotFoundHttpException;
}
}

View File

@@ -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);
}
@@ -261,6 +265,67 @@ class ExpandedForm
return $html;
}
/**
* @param string $name
* @param null $value
* @param array $options
*
* @return string
*/
public function nonSelectableAmount(string $name, $value = null, array $options = []): string
{
$label = $this->label($name, $options);
$options = $this->expandOptionArray($name, $label, $options);
$classes = $this->getHolderClasses($name);
$value = $this->fillFieldValue($name, $value);
$options['step'] = 'any';
$options['min'] = '0.01';
$selectedCurrency = isset($options['currency']) ? $options['currency'] : Amt::getDefaultCurrency();
unset($options['currency']);
unset($options['placeholder']);
// make sure value is formatted nicely:
if (!is_null($value) && $value !== '') {
$value = round($value, $selectedCurrency->decimal_places);
}
$html = view('form.non-selectable-amount', compact('selectedCurrency', 'classes', 'name', 'label', 'value', 'options'))->render();
return $html;
}
/**
* @param string $name
* @param null $value
* @param array $options
*
* @return string
*/
public function nonSelectableBalance(string $name, $value = null, array $options = []): string
{
$label = $this->label($name, $options);
$options = $this->expandOptionArray($name, $label, $options);
$classes = $this->getHolderClasses($name);
$value = $this->fillFieldValue($name, $value);
$options['step'] = 'any';
$selectedCurrency = isset($options['currency']) ? $options['currency'] : Amt::getDefaultCurrency();
unset($options['currency']);
unset($options['placeholder']);
// make sure value is formatted nicely:
if (!is_null($value) && $value !== '') {
$decimals = $selectedCurrency->decimal_places ?? 2;
$value = round($value, $decimals);
}
$html = view('form.non-selectable-amount', compact('selectedCurrency', 'classes', 'name', 'label', 'value', 'options'))->render();
return $html;
}
/**
* @param $type
* @param $name

View File

@@ -64,6 +64,7 @@ class Search implements SearchInterface
if (strlen($string) === 0) {
return is_string($this->originalQuery) ? $this->originalQuery : '';
}
return $string;
}

View File

@@ -0,0 +1,63 @@
<?php
/**
* Account.php
* 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.
*/
declare(strict_types=1);
namespace FireflyIII\Support\Twig;
use FireflyIII\Models\Account as AccountModel;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Support\Facades\Amount as AmountFacade;
use Twig_Extension;
use Twig_SimpleFunction;
/**
* Class Account
*
* @package FireflyIII\Support\Twig
*/
class Account extends Twig_Extension
{
/**
* {@inheritDoc}
*/
public function getFunctions(): array
{
return [
$this->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']]
);
}
}

View File

@@ -15,6 +15,7 @@ declare(strict_types = 1);
namespace FireflyIII;
use FireflyIII\Events\RequestedNewPassword;
use FireflyIII\Models\CurrencyExchangeRate;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
@@ -119,6 +120,14 @@ class User extends Authenticatable
return $this->hasMany('FireflyIII\Models\Category');
}
/**
* @return HasMany
*/
public function currencyExchangeRates(): HasMany
{
return $this->hasMany(CurrencyExchangeRate::class);
}
/**
* @return HasMany
*/

48
composer.lock generated
View File

@@ -665,16 +665,16 @@
},
{
"name": "laravel/framework",
"version": "v5.4.17",
"version": "v5.4.19",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "f7675d59e3863a58ecdff1a5ee1dcd0cff788f4b"
"reference": "02444b7450350db17a7607c8a52f7268ebdb0dad"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/f7675d59e3863a58ecdff1a5ee1dcd0cff788f4b",
"reference": "f7675d59e3863a58ecdff1a5ee1dcd0cff788f4b",
"url": "https://api.github.com/repos/laravel/framework/zipball/02444b7450350db17a7607c8a52f7268ebdb0dad",
"reference": "02444b7450350db17a7607c8a52f7268ebdb0dad",
"shasum": ""
},
"require": {
@@ -790,7 +790,7 @@
"framework",
"laravel"
],
"time": "2017-04-03T13:07:39+00:00"
"time": "2017-04-16T13:33:34+00:00"
},
{
"name": "laravelcollective/html",
@@ -974,16 +974,16 @@
},
{
"name": "league/flysystem",
"version": "1.0.37",
"version": "1.0.38",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem.git",
"reference": "78b5cc4feb61a882302df4fbaf63b7662e5e4ccd"
"reference": "4ba6e13f5116204b21c3afdf400ecf2b9eb1c482"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/78b5cc4feb61a882302df4fbaf63b7662e5e4ccd",
"reference": "78b5cc4feb61a882302df4fbaf63b7662e5e4ccd",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/4ba6e13f5116204b21c3afdf400ecf2b9eb1c482",
"reference": "4ba6e13f5116204b21c3afdf400ecf2b9eb1c482",
"shasum": ""
},
"require": {
@@ -1005,12 +1005,12 @@
"league/flysystem-azure": "Allows you to use Windows Azure Blob storage",
"league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching",
"league/flysystem-copy": "Allows you to use Copy.com storage",
"league/flysystem-dropbox": "Allows you to use Dropbox storage",
"league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem",
"league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files",
"league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib",
"league/flysystem-webdav": "Allows you to use WebDAV storage",
"league/flysystem-ziparchive": "Allows you to use ZipArchive adapter"
"league/flysystem-ziparchive": "Allows you to use ZipArchive adapter",
"spatie/flysystem-dropbox": "Allows you to use Dropbox storage"
},
"type": "library",
"extra": {
@@ -1053,7 +1053,7 @@
"sftp",
"storage"
],
"time": "2017-03-22T15:43:14+00:00"
"time": "2017-04-22T18:59:19+00:00"
},
{
"name": "monolog/monolog",
@@ -1583,16 +1583,16 @@
},
{
"name": "swiftmailer/swiftmailer",
"version": "v5.4.6",
"version": "v5.4.7",
"source": {
"type": "git",
"url": "https://github.com/swiftmailer/swiftmailer.git",
"reference": "81fdccfaf8bdc5d5d7a1ef6bb3a61bbb1a6c4a3e"
"reference": "56db4ed32a6d5c9824c3ecc1d2e538f663f47eb4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/81fdccfaf8bdc5d5d7a1ef6bb3a61bbb1a6c4a3e",
"reference": "81fdccfaf8bdc5d5d7a1ef6bb3a61bbb1a6c4a3e",
"url": "https://api.github.com/repos/swiftmailer/swiftmailer/zipball/56db4ed32a6d5c9824c3ecc1d2e538f663f47eb4",
"reference": "56db4ed32a6d5c9824c3ecc1d2e538f663f47eb4",
"shasum": ""
},
"require": {
@@ -1633,7 +1633,7 @@
"mail",
"mailer"
],
"time": "2017-02-13T07:52:53+00:00"
"time": "2017-04-20T17:32:18+00:00"
},
{
"name": "symfony/console",
@@ -3161,12 +3161,12 @@
"version": "0.9.9",
"source": {
"type": "git",
"url": "https://github.com/padraic/mockery.git",
"url": "https://github.com/mockery/mockery.git",
"reference": "6fdb61243844dc924071d3404bb23994ea0b6856"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/padraic/mockery/zipball/6fdb61243844dc924071d3404bb23994ea0b6856",
"url": "https://api.github.com/repos/mockery/mockery/zipball/6fdb61243844dc924071d3404bb23994ea0b6856",
"reference": "6fdb61243844dc924071d3404bb23994ea0b6856",
"shasum": ""
},
@@ -3223,16 +3223,16 @@
},
{
"name": "myclabs/deep-copy",
"version": "1.6.0",
"version": "1.6.1",
"source": {
"type": "git",
"url": "https://github.com/myclabs/DeepCopy.git",
"reference": "5a5a9fc8025a08d8919be87d6884d5a92520cefe"
"reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/5a5a9fc8025a08d8919be87d6884d5a92520cefe",
"reference": "5a5a9fc8025a08d8919be87d6884d5a92520cefe",
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8e6e04167378abf1ddb4d3522d8755c5fd90d102",
"reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102",
"shasum": ""
},
"require": {
@@ -3261,7 +3261,7 @@
"object",
"object graph"
],
"time": "2017-01-26T22:05:40+00:00"
"time": "2017-04-12T18:52:22+00:00"
},
{
"name": "phpdocumentor/reflection-common",

View File

@@ -23,7 +23,7 @@ return [
'is_demo_site' => false,
],
'encryption' => (is_null(env('USE_ENCRYPTION')) || env('USE_ENCRYPTION') === true),
'version' => '4.3.8',
'version' => '4.4.0',
'maxUploadSize' => 5242880,
'allowedMimes' => ['image/png', 'image/jpeg', 'application/pdf'],
'list_length' => 10,
@@ -134,6 +134,8 @@ return [
'category' => 'FireflyIII\Models\Category',
'transaction_type' => 'FireflyIII\Models\TransactionType',
'currency' => 'FireflyIII\Models\TransactionCurrency',
'fromCurrencyCode' => 'FireflyIII\Support\Binder\CurrencyCode',
'toCurrencyCode' => 'FireflyIII\Support\Binder\CurrencyCode',
'limitrepetition' => 'FireflyIII\Models\LimitRepetition',
'budgetlimit' => 'FireflyIII\Models\BudgetLimit',
'piggyBank' => 'FireflyIII\Models\PiggyBank',
@@ -151,6 +153,7 @@ return [
'tagList' => 'FireflyIII\Support\Binder\TagList',
'start_date' => 'FireflyIII\Support\Binder\Date',
'end_date' => 'FireflyIII\Support\Binder\Date',
'date' => 'FireflyIII\Support\Binder\Date',
],
'rule-triggers' => [
'user_action' => 'FireflyIII\Rules\Triggers\UserAction',
@@ -173,6 +176,7 @@ return [
'category_is' => 'FireflyIII\Rules\Triggers\CategoryIs',
'budget_is' => 'FireflyIII\Rules\Triggers\BudgetIs',
'tag_is' => 'FireflyIII\Rules\Triggers\TagIs',
'has_attachments' => 'FireflyIII\Rules\Triggers\HasAttachment',
],
'rule-actions' => [
'set_category' => 'FireflyIII\Rules\Actions\SetCategory',
@@ -207,4 +211,9 @@ return [
'search_modifiers' => ['amount_is', 'amount', 'amount_max', 'amount_min', 'amount_less', 'amount_more', 'source', 'destination', 'category',
'budget', 'bill', 'type', 'date', 'date_before', 'date_after', 'on', 'before', 'after'],
// tag notes has_attachments
'currency_exchange_services' => [
'fixerio' => 'FireflyIII\Services\Currency\FixerIO',
],
'preferred_exchange_service' => 'fixerio',
];

View File

@@ -159,7 +159,7 @@ return [
'ExpandedForm' => [
'is_safe' => [
'date', 'text', 'select', 'balance', 'optionsList', 'checkbox', 'amount', 'tags', 'integer', 'textarea', 'location',
'multiRadio', 'file', 'multiCheckbox', 'staticText', 'amountSmall', 'password',
'multiRadio', 'file', 'multiCheckbox', 'staticText', 'amountSmall', 'password', 'nonSelectableBalance', 'nonSelectableAmount',
],
],
'Form' => [

View File

@@ -36,11 +36,35 @@ $factory->define(
}
);
$factory->define(
FireflyIII\Models\CurrencyExchangeRate::class, function (Faker\Generator $faker) {
return [
'user_id' => 1,
'from_currency_id' => 1,
'to_currency_id' => 2,
'date' => '2017-01-01',
'rate' => '1.5',
'user_rate' => null,
];
}
);
$factory->define(
FireflyIII\Models\TransactionCurrency::class, function (Faker\Generator $faker) {
return [
'name' => $faker->words(1, true),
'code' => 'ABC',
'symbol' => 'x',
];
}
);
$factory->define(
FireflyIII\Models\ImportJob::class, function (Faker\Generator $faker) {
return [
'id' => $faker->numberBetween(1, 10),
'id' => $faker->numberBetween(1, 100),
'user_id' => 1,
'key' => $faker->words(1, true),
'file_type' => 'csv',
@@ -101,7 +125,7 @@ $factory->define(
$factory->define(
FireflyIII\Models\PiggyBank::class, function (Faker\Generator $faker) {
return [
'id' => $faker->numberBetween(1, 10),
'id' => $faker->unique()->numberBetween(100, 10000),
'account_id' => $faker->numberBetween(1, 10),
'name' => $faker->words(3, true),
'target_amount' => '1000.00',
@@ -116,7 +140,7 @@ $factory->define(
$factory->define(
FireflyIII\Models\Tag::class, function (Faker\Generator $faker) {
return [
'id' => $faker->numberBetween(100, 150),
'id' => $faker->unique()->numberBetween(200, 10000),
'user_id' => 1,
'tagMode' => 'nothing',
'tag' => $faker->words(1, true),

View File

@@ -42,13 +42,6 @@ class ChangesForV431 extends Migration
}
);
// change field "start_date" to "startdate"
// Schema::table(
// 'budget_limits', function (Blueprint $table) {
// $table->renameColumn('startdate', 'start_date');
// }
// );
}
/**

View File

@@ -0,0 +1,58 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
/**
* Class ChangesForV440
*/
class ChangesForV440 extends Migration
{
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
if (Schema::hasTable('currency_exchange_rates')) {
Schema::drop('currency_exchange_rates');
}
}
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
if (!Schema::hasTable('currency_exchange_rates')) {
Schema::create(
'currency_exchange_rates', function (Blueprint $table) {
$table->increments('id');
$table->timestamps();
$table->softDeletes();
$table->integer('user_id', false, true);
$table->integer('from_currency_id', false, true);
$table->integer('to_currency_id', false, true);
$table->date('date');
$table->decimal('rate', 22, 12);
$table->decimal('user_rate', 22, 12)->nullable();
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->foreign('from_currency_id')->references('id')->on('transaction_currencies')->onDelete('cascade');
$table->foreign('to_currency_id')->references('id')->on('transaction_currencies')->onDelete('cascade');
}
);
}
//
Schema::table(
'transactions', function (Blueprint $table) {
$table->integer('transaction_currency_id', false, true)->after('description')->nullable();
$table->foreign('transaction_currency_id')->references('id')->on('transaction_currencies')->onDelete('set null');
}
);
}
}

51
phpunit.coverage.specific.xml Executable file
View File

@@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ phpunit.coverage.specific.xml
~ 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.
-->
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="bootstrap/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
beStrictAboutOutputDuringTests="true"
stopOnFailure="true">
<testsuites>
<testsuite name="Feature Tests">
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
<testsuite name="Unit Tests">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
</testsuites>
<filter>
<whitelist addUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./app</directory>
</whitelist>
<blacklist>
<directory>vendor/</directory>
</blacklist>
</filter>
<logging>
<log type="coverage-clover" target="./storage/build/clover-specific.xml" charset="UTF-8"/>
</logging>
<php>
<env name="APP_ENV" value="testing"/>
<env name="CACHE_DRIVER" value="array"/>
<env name="SESSION_DRIVER" value="array"/>
<env name="QUEUE_DRIVER" value="sync"/>
</php>
</phpunit>

View File

@@ -32,7 +32,7 @@
</blacklist>
</filter>
<logging>
<log type="coverage-clover" target="./storage/build/clover.xml" charset="UTF-8"/>
<log type="coverage-clover" target="./storage/build/clover-all.xml" charset="UTF-8"/>
</logging>
<php>
<env name="APP_ENV" value="testing"/>

View File

@@ -25,11 +25,6 @@ body.waiting * {
cursor: progress;
}
.ui-sortable-placeholder {
display: inline-block;
height: 1px;
}
.preferences-box {
border: 1px #ddd solid;
border-radius: 4px 4px 0 0;
@@ -48,12 +43,6 @@ body.waiting * {
margin: 20px auto 0 auto;
}
.ff-error-page > .headline {
float: left;
font-size: 100px;
font-weight: 300;
}
.ff-error-page > .error-content {
margin-left: 190px;
display: block;

View File

@@ -6,7 +6,7 @@
* See the LICENSE file for details.
*/
/** global: Modernizr */
/** global: Modernizr, currencies */
$(document).ready(function () {
"use strict";
@@ -17,4 +17,14 @@ $(document).ready(function () {
}
);
}
// on change currency drop down list:
$('#ffInput_currency_id').change(updateCurrencyItems);
updateCurrencyItems();
});
function updateCurrencyItems() {
var value = $('#ffInput_currency_id').val();
var symbol = currencies[value];
$('.non-selectable-currency-symbol').text(symbol);
}

View File

@@ -6,7 +6,7 @@
* See the LICENSE file for details.
*/
/** global: Modernizr */
/** global: Modernizr, currencies */
$(document).ready(function () {
"use strict";
@@ -17,4 +17,14 @@ $(document).ready(function () {
}
);
}
// on change currency drop down list:
$('#ffInput_currency_id').change(updateCurrencyItems);
});
function updateCurrencyItems() {
var value = $('#ffInput_currency_id').val();
var symbol = currencies[value];
$('.non-selectable-currency-symbol').text(symbol);
}

View File

@@ -15,8 +15,16 @@ $(function () {
if (budgetLimitID > 0) {
lineChart(budgetChartUri, 'budgetOverview');
}
if (budgetLimitID == 0) {
if (budgetLimitID === 0) {
columnChart(budgetChartUri, 'budgetOverview');
}
// other three charts:
pieChart(expenseCategoryUri, 'budget-cat-out');
pieChart(expenseAssetUri, 'budget-asset-out');
pieChart(expenseExpenseUri, 'budget-expense-out');
});

View File

@@ -8,11 +8,15 @@
* See the LICENSE file for details.
*/
/** global: all, current, specific */
/** global: everything, current, specific */
$(function () {
"use strict";
columnChart(all, 'all');
columnChart(current, 'period');
columnChart(specific, 'period-specific-period');
console.log('Getting charts');
columnChart(everything, 'category-everything');
console.log('Specific: ' + specific);
columnChart(specific, 'specific-period');
});

View File

@@ -42,7 +42,8 @@ var defaultChartOptions = {
callbacks: {
label: function (tooltipItem, data) {
"use strict";
return data.datasets[tooltipItem.datasetIndex].label + ': ' + accounting.formatMoney(tooltipItem.yLabel);
return data.datasets[tooltipItem.datasetIndex].label + ': ' +
accounting.formatMoney(tooltipItem.yLabel, data.datasets[tooltipItem.datasetIndex].currency_symbol);
}
}
}

View File

@@ -91,7 +91,7 @@ function doubleYChart(URI, container) {
"use strict";
var colorData = true;
var options = defaultChartOptions;
var options = $.extend(true, {}, defaultChartOptions);
options.scales.yAxes = [
// y axis 0:
{
@@ -141,7 +141,7 @@ function doubleYNonStackedChart(URI, container) {
"use strict";
var colorData = true;
var options = defaultChartOptions;
var options = $.extend(true, {}, defaultChartOptions);
options.scales.yAxes = [
// y axis 0:
{
@@ -186,7 +186,7 @@ function doubleYNonStackedChart(URI, container) {
*/
function columnChart(URI, container) {
"use strict";
console.log('Going to draw column chart for ' + URI + ' in ' + container);
var colorData = true;
var options = defaultChartOptions;
var chartType = 'bar';

View File

@@ -13,6 +13,8 @@
$(function () {
"use strict";
configAccounting(currencySymbol);
$.ajaxSetup({
headers: {
'X-CSRF-Token': $('meta[name="_token"]').attr('content')
@@ -20,7 +22,7 @@ $(function () {
});
// when you click on a currency, this happens:
$('.currency-option').click(currencySelect);
$('.currency-option').on('click', currencySelect);
var ranges = {};
ranges[dateRangeConfig.currentPeriod] = [moment(dateRangeConfig.ranges.current[0]), moment(dateRangeConfig.ranges.current[1])];
@@ -108,10 +110,12 @@ function currencySelect(e) {
return false;
}
function configAccounting(customCurrency) {
// Settings object that controls default parameters for library methods:
accounting.settings = {
currency: {
symbol: currencySymbol, // default currency symbol is '$'
symbol: customCurrency, // default currency symbol is '$'
format: accountingConfig, // controls output: %s = symbol, %v = value/number (can be object: see below)
decimal: mon_decimal_point, // decimal point separator
thousand: mon_thousands_sep, // thousands separator
@@ -123,7 +127,7 @@ accounting.settings = {
decimal: "."
}
};
}
function listLengthInitial() {
"use strict";

View File

@@ -62,7 +62,7 @@ function updateBar(data) {
function reportErrors(data) {
"use strict";
if (data.errors.length == 1) {
if (data.errors.length === 1) {
$('#import-status-error-intro').text(langImportSingleError);
//'An error has occured during the import. The import can continue, however.'
}
@@ -93,7 +93,7 @@ function kickStartJob() {
function updateTimeout(data) {
"use strict";
if (data.stepsDone != stepCount) {
if (data.stepsDone !== stepCount) {
stepCount = data.stepsDone;
currentLimit = 0;
return;

View File

@@ -14,7 +14,7 @@ $(function () {
"use strict";
// do chart JS stuff.
drawChart();
if (showTour == true) {
if (showTour === true) {
$.getJSON('json/tour').done(function (data) {
var tour = new Tour(
{

View File

@@ -99,9 +99,9 @@ function displayAjaxPartial(data, holder) {
function failAjaxPartial(uri, holder) {
"use strict";
var holder = $('#' + holder);
holder.parent().find('.overlay').remove();
holder.addClass('general-chart-error');
var holderObject = $('#' + holder);
holderObject.parent().find('.overlay').remove();
holderObject.addClass('general-chart-error');
}

View File

@@ -56,7 +56,7 @@ $(function () {
// set date from cookie
var startStr = readCookie('report-start');
var endStr = readCookie('report-end');
if (startStr !== null && endStr !== null && startStr.length == 8 && endStr.length == 8) {
if (startStr !== null && endStr !== null && startStr.length === 8 && endStr.length === 8) {
var startDate = moment(startStr, "YYYY-MM-DD");
var endDate = moment(endStr, "YYYY-MM-DD");
var datePicker = $('#inputDateRange').data('daterangepicker');

View File

@@ -104,14 +104,14 @@ function addNewAction() {
function removeTrigger(e) {
"use strict";
var target = $(e.target);
if (target.prop("tagName") == "I") {
if (target.prop("tagName") === "I") {
target = target.parent();
}
// remove grand parent:
target.parent().parent().remove();
// if now at zero, immediatly add one again:
if ($('.rule-trigger-tbody tr').length == 0) {
if ($('.rule-trigger-tbody tr').length === 0) {
addNewTrigger();
}
return false;
@@ -125,14 +125,14 @@ function removeTrigger(e) {
function removeAction(e) {
"use strict";
var target = $(e.target);
if (target.prop("tagName") == "I") {
if (target.prop("tagName") === "I") {
target = target.parent();
}
// remove grand parent:
target.parent().parent().remove();
// if now at zero, immediatly add one again:
if ($('.rule-action-tbody tr').length == 0) {
if ($('.rule-action-tbody tr').length === 0) {
addNewAction();
}
return false;

View File

@@ -71,7 +71,7 @@ function initialize() {
/*
Maybe place marker?
*/
if(doPlaceMarker == true) {
if (doPlaceMarker === true) {
var myLatlng = new google.maps.LatLng(latitude, longitude);
var fakeEvent = {};
fakeEvent.latLng = myLatlng;

42
public/js/ff/tags/show.js Normal file
View File

@@ -0,0 +1,42 @@
/*
* show.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.
*/
/*
Some vars as prep for the map:
*/
var map;
var markers = [];
var setTag = false;
var mapOptions = {
zoom: zoomLevel,
center: new google.maps.LatLng(latitude, longitude),
disableDefaultUI: true,
zoomControl: false,
scaleControl: true,
draggable: false
};
function initialize() {
"use strict";
if (doPlaceMarker === true) {
/*
Create new map:
*/
map = new google.maps.Map(document.getElementById('map-canvas'), mapOptions);
var myLatlng = new google.maps.LatLng(latitude, longitude);
var marker = new google.maps.Marker({
position: myLatlng,
map: map
});
marker.setMap(map);
}
}
google.maps.event.addDomListener(window, 'load', initialize);

View File

@@ -72,8 +72,8 @@ function countChecked() {
"use strict";
var checked = $('.select_all_single:checked').length;
if (checked > 0) {
$('.mass_edit span').text(edit_selected_txt + ' (' + checked + ')')
$('.mass_delete span').text(delete_selected_txt + ' (' + checked + ')')
$('.mass_edit span').text(edit_selected_txt + ' (' + checked + ')');
$('.mass_delete span').text(delete_selected_txt + ' (' + checked + ')');
$('.mass_button_options').show();
} else {

View File

@@ -0,0 +1,206 @@
/*
* common.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.
*/
$(document).ready(function () {
"use strict";
setCommonAutocomplete();
runModernizer();
});
/**
* Give date a datepicker if not natively supported.
*/
function runModernizer() {
if (!Modernizr.inputtypes.date) {
$('input[type="date"]').datepicker(
{
dateFormat: 'yy-mm-dd'
}
);
}
}
/**
* Auto complete things in both edit and create routines:
*/
function setCommonAutocomplete() {
$.getJSON('json/tags').done(function (data) {
var opt = {
typeahead: {
source: data,
afterSelect: function () {
this.$element.val("");
}
}
};
$('input[name="tags"]').tagsinput(
opt
);
});
if ($('input[name="destination_account_name"]').length > 0) {
$.getJSON('json/expense-accounts').done(function (data) {
$('input[name="destination_account_name"]').typeahead({source: data});
});
}
if ($('input[name="source_account_name"]').length > 0) {
$.getJSON('json/revenue-accounts').done(function (data) {
$('input[name="source_account_name"]').typeahead({source: data});
});
}
$.getJSON('json/categories').done(function (data) {
$('input[name="category"]').typeahead({source: data});
});
}
/**
* When the user changes the currency in the amount drop down, it may jump from being
* the native currency to a foreign currency. This triggers the display of several
* information things that make sure that the user always supplies the amount in the native currency.
*
* @returns {boolean}
*/
function selectsForeignCurrency() {
var foreignCurrencyId = parseInt($('input[name="amount_currency_id_amount"]').val());
var selectedAccountId = getAccountId();
var nativeCurrencyId = parseInt(accountInfo[selectedAccountId].preferredCurrency);
if (foreignCurrencyId !== nativeCurrencyId) {
console.log('User has selected currency #' + foreignCurrencyId + ' and this is different from native currency #' + nativeCurrencyId);
// the input where the native amount is entered gets the symbol for the native currency:
$('.non-selectable-currency-symbol').text(currencyInfo[nativeCurrencyId].symbol);
// the instructions get updated:
$('#ffInput_exchange_rate_instruction').text(getExchangeInstructions());
// both holders are shown to the user:
$('#exchange_rate_instruction_holder').show();
$('#native_amount_holder').show();
// if possible the amount is already exchanged for the foreign currency
convertForeignToNative();
}
if (foreignCurrencyId === nativeCurrencyId) {
console.log('User has selected currency #' + foreignCurrencyId + ' and this is equal to native currency #' + nativeCurrencyId + ' (phew).');
$('#exchange_rate_instruction_holder').hide();
$('#native_amount_holder').hide();
}
return false;
}
/**
* Converts any foreign amount to the native currency.
*/
function convertForeignToNative() {
var accountId = getAccountId();
var foreignCurrencyId = parseInt($('input[name="amount_currency_id_amount"]').val());
var nativeCurrencyId = parseInt(accountInfo[accountId].preferredCurrency);
var foreignCurrencyCode = currencyInfo[foreignCurrencyId].code;
var nativeCurrencyCode = currencyInfo[nativeCurrencyId].code;
var date = $('#ffInput_date').val();
var amount = $('#ffInput_amount').val();
var uri = 'json/rate/' + foreignCurrencyCode + '/' + nativeCurrencyCode + '/' + date + '?amount=' + amount;
console.log('Will grab ' + uri);
$.get(uri).done(updateNativeAmount);
}
/**
* Once the data has been grabbed will update the field in the form.
* @param data
*/
function updateNativeAmount(data) {
console.log('Returned data:');
console.log(data);
$('#ffInput_native_amount').val(data.amount);
}
/**
* Instructions for transfers
*/
function getTransferExchangeInstructions() {
var sourceAccount = $('select[name="source_account_id"]').val();
var destAccount = $('select[name="destination_account_id"]').val();
var sourceCurrency = accountInfo[sourceAccount].preferredCurrency;
var destinationCurrency = accountInfo[destAccount].preferredCurrency;
return transferInstructions.replace('@source_name', accountInfo[sourceAccount].name)
.replace('@dest_name', accountInfo[destAccount].name)
.replace(/@source_currency/g, currencyInfo[sourceCurrency].name)
.replace(/@dest_currency/g, currencyInfo[destinationCurrency].name);
}
/**
* When the transaction to create is a transfer some more checks are necessary.
*/
function validateCurrencyForTransfer() {
if (what !== "transfer") {
return;
}
$('#source_amount_holder').show();
var sourceAccount = $('select[name="source_account_id"]').val();
var destAccount = $('select[name="destination_account_id"]').val();
var sourceCurrency = accountInfo[sourceAccount].preferredCurrency;
var sourceSymbol = currencyInfo[sourceCurrency].symbol;
var destinationCurrency = accountInfo[destAccount].preferredCurrency;
var destinationSymbol = currencyInfo[destinationCurrency].symbol;
$('#source_amount_holder').show().find('.non-selectable-currency-symbol').text(sourceSymbol);
if (sourceCurrency === destinationCurrency) {
console.log('Both accounts accept ' + sourceCurrency);
$('#destination_amount_holder').hide();
$('#amount_holder').hide();
return;
}
console.log('Source accepts #' + sourceCurrency + ', destination #' + destinationCurrency);
$('#ffInput_exchange_rate_instruction').text(getTransferExchangeInstructions());
$('#exchange_rate_instruction_holder').show();
$('input[name="source_amount"]').val($('input[name="amount"]').val());
convertSourceToDestination();
$('#destination_amount_holder').show().find('.non-selectable-currency-symbol').text(destinationSymbol);
$('#amount_holder').hide();
}
/**
* Convert from source amount currency to destination currency for transfers.
*
*/
function convertSourceToDestination() {
var sourceAccount = $('select[name="source_account_id"]').val();
var destAccount = $('select[name="destination_account_id"]').val();
var sourceCurrency = accountInfo[sourceAccount].preferredCurrency;
var destinationCurrency = accountInfo[destAccount].preferredCurrency;
var sourceCurrencyCode = currencyInfo[sourceCurrency].code;
var destinationCurrencyCode = currencyInfo[destinationCurrency].code;
var date = $('#ffInput_date').val();
var amount = $('#ffInput_source_amount').val();
$('#ffInput_amount').val(amount);
var uri = 'json/rate/' + sourceCurrencyCode + '/' + destinationCurrencyCode + '/' + date + '?amount=' + amount;
console.log('Will grab ' + uri);
$.get(uri).done(updateDestinationAmount);
}
/**
* Once the data has been grabbed will update the field (for transfers)
* @param data
*/
function updateDestinationAmount(data) {
console.log('Returned data:');
console.log(data);
$('#ffInput_destination_amount').val(data.amount);
}

View File

@@ -6,88 +6,85 @@
* See the LICENSE file for details.
*/
/** global: what,Modernizr, title, breadcrumbs, middleCrumbName, button, piggiesLength, txt, middleCrumbUrl */
/** global: what,Modernizr, title, breadcrumbs, middleCrumbName, button, piggiesLength, txt, middleCrumbUrl,exchangeRateInstructions */
$(document).ready(function () {
"use strict";
// respond to switch buttons
// hide ALL exchange things and AMOUNT things
$('#exchange_rate_instruction_holder').hide();
$('#native_amount_holder').hide();
$('#amount_holder').hide();
$('#source_amount_holder').hide();
$('#destination_amount_holder').hide();
// respond to switch buttons (first time always triggers)
updateButtons();
updateForm();
updateLayout();
updateDescription();
updateNativeCurrency();
if (!Modernizr.inputtypes.date) {
$('input[type="date"]').datepicker(
{
dateFormat: 'yy-mm-dd'
}
);
}
// update currency
$('select[name="source_account_id"]').on('change', updateCurrency)
// when user changes source account or destination, native currency may be different.
$('select[name="source_account_id"]').on('change', updateNativeCurrency);
$('select[name="destination_account_id"]').on('change', updateNativeCurrency);
// get JSON things:
getJSONautocomplete();
// convert foreign currency to native currency (when input changes, exchange rate)
$('#ffInput_amount').on('change', convertForeignToNative);
// convert source currency to destination currency (slightly different routine for transfers)
$('#ffInput_source_amount').on('change', convertSourceToDestination);
// when user selects different currency,
$('.currency-option').on('click', selectsForeignCurrency);
});
/**
* This function generates a small helper text to explain the user
* that they have selected a foreign currency.
* @returns {XML|string|void}
*/
function getExchangeInstructions() {
var foreignCurrencyId = parseInt($('input[name="amount_currency_id_amount"]').val());
var selectedAccountId = getAccountId();
var nativeCurrencyId = parseInt(accountInfo[selectedAccountId].preferredCurrency);
function updateCurrency() {
// get value:
var accountId = $('select[name="source_account_id"]').val();
console.log('account id is ' + accountId);
var currencyPreference = accountInfo[accountId].preferredCurrency;
console.log('currency pref is ' + currencyPreference);
var text = exchangeRateInstructions.replace('@name', accountInfo[selectedAccountId].name);
text = text.replace(/@native_currency/g, currencyInfo[nativeCurrencyId].name);
text = text.replace(/@foreign_currency/g, currencyInfo[foreignCurrencyId].name);
return text;
}
$('.currency-option[data-id="' + currencyPreference + '"]').click();
/**
* There is an input that shows the currency symbol that is native to the selected
* acccount. So when the user changes the selected account, the native currency is updated:
*/
function updateNativeCurrency() {
var newAccountId = getAccountId();
var nativeCurrencyId = accountInfo[newAccountId].preferredCurrency;
console.log('User selected account #' + newAccountId + '. Native currency is #' + nativeCurrencyId);
$('.currency-option[data-id="' + nativeCurrencyId + '"]').click();
$('[data-toggle="dropdown"]').parent().removeClass('open');
$('select[name="source_account_id"]').focus();
validateCurrencyForTransfer();
}
/**
*
*/
function updateDescription() {
$.getJSON('json/transaction-journals/' + what).done(function (data) {
$('input[name="description"]').typeahead('destroy').typeahead({source: data});
});
}
function getJSONautocomplete() {
// for withdrawals
$.getJSON('json/expense-accounts').done(function (data) {
$('input[name="destination_account_name"]').typeahead({source: data});
});
// for tags:
if ($('input[name="tags"]').length > 0) {
$.getJSON('json/tags').done(function (data) {
var opt = {
typeahead: {
source: data,
afterSelect: function () {
this.$element.val("");
}
}
};
$('input[name="tags"]').tagsinput(
opt
);
});
}
// for deposits
$.getJSON('json/revenue-accounts').done(function (data) {
$('input[name="source_account_name"]').typeahead({source: data});
});
$.getJSON('json/categories').done(function (data) {
$('input[name="category"]').typeahead({source: data});
});
}
/**
*
*/
function updateLayout() {
"use strict";
$('#subTitle').text(title[what]);
@@ -96,78 +93,96 @@ function updateLayout() {
$('#transaction-btn').text(button[what]);
}
/**
*
*/
function updateForm() {
"use strict";
$('input[name="what"]').val(what);
var destName = $('#ffInput_destination_account_name');
var srcName = $('#ffInput_source_account_name');
switch (what) {
case 'withdrawal':
// show source_id and dest_name:
$('#source_account_id_holder').show();
$('#destination_account_name_holder').show();
// show source_id and dest_name
document.getElementById('source_account_id_holder').style.display = 'block';
document.getElementById('destination_account_name_holder').style.display = 'block';
// hide others:
$('#source_account_name_holder').hide();
$('#destination_account_id_holder').hide();
// show budget:
$('#budget_id_holder').show();
document.getElementById('source_account_name_holder').style.display = 'none';
document.getElementById('destination_account_id_holder').style.display = 'none';
document.getElementById('budget_id_holder').style.display = 'block';
// hide piggy bank:
$('#piggy_bank_id_holder').hide();
document.getElementById('piggy_bank_id_holder').style.display = 'none';
// copy destination account name to
// source account name:
if ($('#ffInput_destination_account_name').val().length > 0) {
$('#ffInput_source_account_name').val($('#ffInput_destination_account_name').val());
// copy destination account name to source account name:
if (destName.val().length > 0) {
srcName.val(destName.val());
}
// exchange / foreign currencies:
// hide explanation, hide source and destination amounts, show normal amount
document.getElementById('exchange_rate_instruction_holder').style.display = 'none';
document.getElementById('source_amount_holder').style.display = 'none';
document.getElementById('destination_amount_holder').style.display = 'none';
document.getElementById('amount_holder').style.display = 'block';
break;
case 'deposit':
// show source_name and dest_id:
$('#source_account_name_holder').show();
$('#destination_account_id_holder').show();
document.getElementById('source_account_name_holder').style.display = 'block';
document.getElementById('destination_account_id_holder').style.display = 'block';
// hide others:
$('#source_account_id_holder').hide();
$('#destination_account_name_holder').hide();
document.getElementById('source_account_id_holder').style.display = 'none';
document.getElementById('destination_account_name_holder').style.display = 'none';
// hide budget
$('#budget_id_holder').hide();
document.getElementById('budget_id_holder').style.display = 'none';
// hide piggy bank
$('#piggy_bank_id_holder').hide();
document.getElementById('piggy_bank_id_holder').style.display = 'none';
if ($('#ffInput_source_account_name').val().length > 0) {
$('#ffInput_destination_account_name').val($('#ffInput_source_account_name').val());
// copy name
if (srcName.val().length > 0) {
destName.val(srcName.val());
}
// exchange / foreign currencies:
// hide explanation, hide source and destination amounts, show amount
document.getElementById('exchange_rate_instruction_holder').style.display = 'none';
document.getElementById('source_amount_holder').style.display = 'none';
document.getElementById('destination_amount_holder').style.display = 'none';
document.getElementById('amount_holder').style.display = 'block';
break;
case 'transfer':
// show source_id and dest_id:
$('#source_account_id_holder').show();
$('#destination_account_id_holder').show();
document.getElementById('source_account_id_holder').style.display = 'block';
document.getElementById('destination_account_id_holder').style.display = 'block';
// hide others:
$('#source_account_name_holder').hide();
$('#destination_account_name_holder').hide();
document.getElementById('source_account_name_holder').style.display = 'none';
document.getElementById('destination_account_name_holder').style.display = 'none';
// hide budget
$('#budget_id_holder').hide();
document.getElementById('budget_id_holder').style.display = 'none';
// optional piggies
var showPiggies = 'block';
if (piggiesLength === 0) {
$('#piggy_bank_id_holder').hide();
} else {
$('#piggy_bank_id_holder').show();
showPiggies = 'none';
}
break;
default:
// no action.
document.getElementById('piggy_bank_id_holder').style.display = showPiggies;
break;
}
updateNativeCurrency();
}
/**
*
*/
function updateButtons() {
"use strict";
$('.switch').each(function (i, v) {
@@ -178,7 +193,7 @@ function updateButtons() {
// new click event:
button.bind('click', clickButton);
if (button.data('what') == what) {
if (button.data('what') === what) {
button.removeClass('btn-default').addClass('btn-info').html('<i class="fa fa-fw fa-check"></i> ' + txt[button.data('what')]);
} else {
button.removeClass('btn-info').addClass('btn-default').text(txt[button.data('what')]);
@@ -186,11 +201,16 @@ function updateButtons() {
});
}
/**
*
* @param e
* @returns {boolean}
*/
function clickButton(e) {
"use strict";
var button = $(e.target);
var newWhat = button.data('what');
if (newWhat != what) {
if (newWhat !== what) {
what = newWhat;
updateButtons();
updateForm();
@@ -199,3 +219,15 @@ function clickButton(e) {
}
return false;
}
/**
* Get accountID based on some meta info.
*/
function getAccountId() {
if (what === "withdrawal") {
return $('select[name="source_account_id"]').val();
}
if (what === "deposit" || what === "transfer") {
return $('select[name="destination_account_id"]').val();
}
}

Some files were not shown because too many files have changed in this diff Show More