Merge branch 'release/4.6.4'

This commit is contained in:
James Cole
2017-08-13 09:04:40 +02:00
326 changed files with 8023 additions and 5636 deletions

View File

@@ -1,6 +1,5 @@
language: php
php:
- 7.0
- 7.1
cache:

View File

@@ -2,11 +2,50 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## [4.6.4] - 2017-08-13
### Added
- PHP7.1 support
- Routine to decrypt attachments from the command line, for issue #671
- A routine that can check if your password has been stolen in the past.
- Split transaction shows amount left to be split
### Changed
- Importer can (potentially) handle new import routines such as banks.
- Importer can fall back from JSON errors
### Deprecated
- Initial release.
### Removed
- PHP7.0 support
- Support for extended tag modes
- Remove "time jumps" to non-empty periods
### Fixed
- #717, reported by @NiceGuyIT
- #718, reported by @wtercato
- #722, reported by @simonsmiley
- #648, reported by @skibbipl
- #730, reported by @ragnarkarlsson
- #733, reported by @xpfgsyb
- #735, reported by @kristophr
- #739, reported by @skibbipl
- #515, reported by @schwalberich
- #743, reported by @simonsmiley
- #746, reported by @tannie
- #747, reported by @tannie
### Security
- Initial release.
## [4.6.3.1] - 2017-07-23
### Fixed
- Hotfix to close issue #715
## [4.6.3] - 2017-07-23
This will be the last release to support PHP 7.0.

View File

@@ -1,6 +1,6 @@
# Firefly III: A personal finances manager
[![Requires PHP7](https://img.shields.io/badge/php-7.0-red.svg)](https://secure.php.net/downloads.php) [![Latest Stable Version](https://poser.pugx.org/grumpydictator/firefly-iii/v/stable)](https://packagist.org/packages/grumpydictator/firefly-iii) [![License](https://img.shields.io/badge/license-CC%20BY--SA%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by-sa/4.0/) [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=44UKUT455HUFA)
[![Requires PHP7.1](https://img.shields.io/badge/php-7.1-red.svg)](https://secure.php.net/downloads.php) [![Latest Stable Version](https://poser.pugx.org/grumpydictator/firefly-iii/v/stable)](https://packagist.org/packages/grumpydictator/firefly-iii) [![License](https://img.shields.io/badge/license-CC%20BY--SA%204.0-lightgrey.svg)](https://creativecommons.org/licenses/by-sa/4.0/) [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=44UKUT455HUFA)
[![The index of Firefly III](https://i.nder.be/hurdhgyg/400)](https://i.nder.be/h2b37243) [![The account overview of Firefly III](https://i.nder.be/hnkfkdpr/400)](https://i.nder.be/hv70pbwc)

View File

@@ -0,0 +1,104 @@
<?php
/**
* DecryptAttachment.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\Console\Commands;
use FireflyIII\Repositories\Attachment\AttachmentRepositoryInterface;
use Illuminate\Console\Command;
use Log;
/**
* Class DecryptAttachment
*
* @package FireflyIII\Console\Commands
*/
class DecryptAttachment extends Command
{
/**
* The console command description.
*
* @var string
*/
protected $description = 'Decrypts an attachment and dumps the content in a file in the given directory.';
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature
= 'firefly:decrypt-attachment {id:The ID of the attachment.} {name:The file name of the attachment.}
{directory:Where the file must be stored.}';
/**
* Create a new command instance.
*
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*/
public function handle()
{
/** @var AttachmentRepositoryInterface $repository */
$repository = app(AttachmentRepositoryInterface::class);
$attachmentId = intval($this->argument('id'));
$attachment = $repository->findWithoutUser($attachmentId);
$attachmentName = trim($this->argument('name'));
$storagePath = realpath(trim($this->argument('directory')));
if (is_null($attachment->id)) {
$this->error(sprintf('No attachment with id #%d', $attachmentId));
Log::error(sprintf('DecryptAttachment: No attachment with id #%d', $attachmentId));
return;
}
if ($attachmentName !== $attachment->filename) {
$this->error('File name does not match.');
Log::error('DecryptAttachment: File name does not match.');
return;
}
if (!is_dir($storagePath)) {
$this->error(sprintf('Path "%s" is not a directory.', $storagePath));
Log::error(sprintf('DecryptAttachment: Path "%s" is not a directory.', $storagePath));
return;
}
if (!is_writable($storagePath)) {
$this->error(sprintf('Path "%s" is not writable.', $storagePath));
Log::error(sprintf('DecryptAttachment: Path "%s" is not writable.', $storagePath));
return;
}
$fullPath = $storagePath . DIRECTORY_SEPARATOR . $attachment->filename;
$content = $repository->getContent($attachment);
$this->line(sprintf('Going to write content for attachment #%d into file "%s"', $attachment->id, $fullPath));
$result = file_put_contents($fullPath, $content);
if ($result === false) {
$this->error('Could not write to file.');
return;
}
$this->info(sprintf('%d bytes written. Exiting now..', $result));
return;
}
}

View File

@@ -1,10 +1,22 @@
<?php
declare(strict_types=1);
/**
* UseEncryption.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.
*/
namespace FireflyIII\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
/**
* Class UseEncryption
*/
class UseEncryption extends Command
{
/**

View File

@@ -14,6 +14,7 @@ declare(strict_types=1);
namespace FireflyIII\Console;
use FireflyIII\Console\Commands\CreateImport;
use FireflyIII\Console\Commands\DecryptAttachment;
use FireflyIII\Console\Commands\EncryptFile;
use FireflyIII\Console\Commands\Import;
use FireflyIII\Console\Commands\ScanAttachments;
@@ -63,6 +64,7 @@ class Kernel extends ConsoleKernel
ScanAttachments::class,
UpgradeDatabase::class,
UseEncryption::class,
DecryptAttachment::class,
];
/**

View File

@@ -45,11 +45,11 @@ final class Entry
public $transaction_type;
public $source_account_id;
public $source_account_name;
public $asset_account_id;
public $asset_account_name;
public $destination_account_id;
public $destination_account_name;
public $opposing_account_id;
public $opposing_account_name;
public $budget_id;
public $budget_name;
@@ -71,21 +71,21 @@ final class Entry
*/
public static function fromObject($object): Entry
{
$entry = new self;
$entry->journal_id = $object->transaction_journal_id;
$entry->description = Steam::decrypt(intval($object->journal_encrypted), $object->journal_description);
$entry->amount = $object->amount;
$entry->date = $object->date;
$entry->transaction_type = $object->transaction_type;
$entry->currency_code = $object->transaction_currency_code;
$entry->source_account_id = $object->account_id;
$entry->source_account_name = Steam::decrypt(intval($object->account_name_encrypted), $object->account_name);
$entry->destination_account_id = $object->opposing_account_id;
$entry->destination_account_name = Steam::decrypt(intval($object->opposing_account_encrypted), $object->opposing_account_name);
$entry->category_id = $object->category_id ?? '';
$entry->category_name = $object->category_name ?? '';
$entry->budget_id = $object->budget_id ?? '';
$entry->budget_name = $object->budget_name ?? '';
$entry = new self;
$entry->journal_id = $object->transaction_journal_id;
$entry->description = Steam::decrypt(intval($object->journal_encrypted), $object->journal_description);
$entry->amount = $object->amount;
$entry->date = $object->date;
$entry->transaction_type = $object->transaction_type;
$entry->currency_code = $object->transaction_currency_code;
$entry->asset_account_id = $object->account_id;
$entry->asset_account_name = Steam::decrypt(intval($object->account_name_encrypted), $object->account_name);
$entry->opposing_account_id = $object->opposing_account_id;
$entry->opposing_account_name = Steam::decrypt(intval($object->opposing_account_encrypted), $object->opposing_account_name);
$entry->category_id = $object->category_id ?? '';
$entry->category_name = $object->category_name ?? '';
$entry->budget_id = $object->budget_id ?? '';
$entry->budget_name = $object->budget_name ?? '';
// update description when transaction description is different:
if (!is_null($object->description) && $object->description !== $entry->description) {

View File

@@ -99,7 +99,7 @@ class AttachmentHelper implements AttachmentHelperInterface
*
* @return bool
*/
public function saveAttachmentsForModel(Model $model, array $files = null): bool
public function saveAttachmentsForModel(Model $model, ?array $files): bool
{
if (is_array($files)) {
foreach ($files as $entry) {

View File

@@ -55,6 +55,6 @@ interface AttachmentHelperInterface
*
* @return bool
*/
public function saveAttachmentsForModel(Model $model, array $files = null): bool;
public function saveAttachmentsForModel(Model $model, ?array $files): bool;
}

View File

@@ -64,22 +64,22 @@ class JournalCollector implements JournalCollectorInterface
'transaction_journals.bill_id',
'bills.name as bill_name',
'bills.name_encrypted as bill_name_encrypted',
'transactions.id as id',
'transactions.id as id',
'transactions.description as transaction_description',
'transactions.account_id',
'transactions.identifier',
'transactions.transaction_journal_id',
'transactions.amount as transaction_amount',
'transactions.transaction_currency_id as transaction_currency_id',
'transaction_currencies.code as transaction_currency_code',
'transaction_currencies.symbol as transaction_currency_symbol',
'transaction_currencies.decimal_places as transaction_currency_dp',
'transactions.foreign_amount as transaction_foreign_amount',
'transactions.foreign_currency_id as foreign_currency_id',
'foreign_currencies.code as foreign_currency_code',
'foreign_currencies.symbol as foreign_currency_symbol',
'foreign_currencies.decimal_places as foreign_currency_dp',
@@ -399,6 +399,10 @@ class JournalCollector implements JournalCollectorInterface
*/
public function setPage(int $page): JournalCollectorInterface
{
if ($page < 1) {
$page = 1;
}
$this->page = $page;
if ($page > 0) {
@@ -673,6 +677,7 @@ class JournalCollector implements JournalCollectorInterface
$this->query->leftJoin('account_types as opposing_account_types', 'opposing_accounts.account_type_id', '=', 'opposing_account_types.id');
$this->query->whereNull('opposing.deleted_at');
$this->fields[] = 'opposing.id as opposing_id';
$this->fields[] = 'opposing.account_id as opposing_account_id';
$this->fields[] = 'opposing_accounts.name as opposing_account_name';
$this->fields[] = 'opposing_accounts.encrypted as opposing_account_encrypted';

View File

@@ -41,11 +41,13 @@ class TransferFilter implements FilterInterface
continue;
}
// make property string:
$journalId = $transaction->transaction_journal_id;
$amount = Steam::positive($transaction->transaction_amount);
$accountIds = [intval($transaction->account_id), intval($transaction->opposing_account_id)];
$journalId = $transaction->transaction_journal_id;
$amount = Steam::positive($transaction->transaction_amount);
$accountIds = [intval($transaction->account_id), intval($transaction->opposing_account_id)];
$transactionIds = [$transaction->id, intval($transaction->opposing_id)];
sort($accountIds);
$key = $journalId . '-' . join(',', $accountIds) . '-' . $amount;
sort($transactionIds);
$key = $journalId . '-' . join(',', $transactionIds) . '-' . join(',', $accountIds) . '-' . $amount;
if (!isset($count[$key])) {
// not yet counted? add to new set and count it:
$new->push($transaction);

View File

@@ -253,7 +253,7 @@ class AccountController extends Controller
$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'));
$page = intval($request->get('page'));
$pageSize = intval(Preferences::get('transactionPageSize', 50)->data);
$chartUri = route('chart.account.single', [$account->id]);
$start = null;
@@ -274,55 +274,31 @@ class AccountController extends Controller
if (strlen($moment) > 0 && $moment !== 'all') {
$start = new Carbon($moment);
$end = Navigation::endOfPeriod($start, $range);
$subTitle = trans(
'firefly.journals_in_period_for_account', ['name' => $account->name, 'start' => $start->formatLocalized($this->monthAndDayFormat),
'end' => $end->formatLocalized($this->monthAndDayFormat)]
);
$fStart = $start->formatLocalized($this->monthAndDayFormat);
$fEnd = $end->formatLocalized($this->monthAndDayFormat);
$subTitle = trans('firefly.journals_in_period_for_account', ['name' => $account->name, 'start' => $fStart, 'end' => $fEnd]);
$chartUri = route('chart.account.period', [$account->id, $start->format('Y-m-d')]);
$periods = $this->getPeriodOverview($account);
}
// prep for current period
// prep for current period view
if (strlen($moment) === 0) {
$start = clone session('start', Navigation::startOfPeriod(new Carbon, $range));
$end = clone session('end', Navigation::endOfPeriod(new Carbon, $range));
$subTitle = trans(
'firefly.journals_in_period_for_account', ['name' => $account->name, 'start' => $start->formatLocalized($this->monthAndDayFormat),
'end' => $end->formatLocalized($this->monthAndDayFormat)]
);
$fStart = $start->formatLocalized($this->monthAndDayFormat);
$fEnd = $end->formatLocalized($this->monthAndDayFormat);
$subTitle = trans('firefly.journals_in_period_for_account', ['name' => $account->name, 'start' => $fStart, 'end' => $fEnd]);
$periods = $this->getPeriodOverview($account);
}
$count = 0;
$loop = 0;
// grab journals, but be prepared to jump a period back to get the right ones:
Log::info('Now at loop start.');
while ($count === 0 && $loop < 3) {
$loop++;
$collector = app(JournalCollectorInterface::class);
Log::info('Count is zero, search for journals.');
$collector->setAccounts(new Collection([$account]))->setLimit($pageSize)->setPage($page);
if (!is_null($start)) {
$collector->setRange($start, $end);
}
$journals = $collector->getPaginatedJournals();
$journals->setPath('accounts/show/' . $account->id . '/' . $moment);
$count = $journals->getCollection()->count();
if ($count === 0 && $loop < 3) {
$start->subDay();
$start = Navigation::startOfPeriod($start, $range);
$end = Navigation::endOfPeriod($start, $range);
Log::info(sprintf('Count is still zero, go back in time to "%s" and "%s"!', $start->format('Y-m-d'), $end->format('Y-m-d')));
}
// grab journals:
$collector = app(JournalCollectorInterface::class);
$collector->setAccounts(new Collection([$account]))->setLimit($pageSize)->setPage($page);
if (!is_null($start)) {
$collector->setRange($start, $end);
}
if ($moment !== 'all' && $loop > 1) {
$subTitle = trans(
'firefly.journals_in_period_for_account', ['name' => $account->name, 'start' => $start->formatLocalized($this->monthAndDayFormat),
'end' => $end->formatLocalized($this->monthAndDayFormat)]
);
}
$journals = $collector->getPaginatedJournals();
$journals->setPath(route('accounts.show', [$account->id, $moment]));
return view(
'accounts.show',
@@ -421,7 +397,7 @@ class AccountController extends Controller
$start = $repository->oldestJournalDate($account);
$range = Preferences::get('viewRange', '1M')->data;
$start = Navigation::startOfPeriod($start, $range);
$end = Navigation::endOfX(new Carbon, $range);
$end = Navigation::endOfX(new Carbon, $range, null);
$entries = new Collection;
// properties for cache

View File

@@ -17,6 +17,7 @@ use Config;
use FireflyConfig;
use FireflyIII\Events\RegisteredUser;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Http\Requests\UserRegistrationRequest;
use FireflyIII\User;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Http\Request;
@@ -52,11 +53,11 @@ class RegisterController extends Controller
}
/**
* @param Request $request
* @param UserRegistrationRequest|Request $request
*
* @return \Illuminate\Contracts\View\Factory|\Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|\Illuminate\View\View
*/
public function register(Request $request)
public function register(UserRegistrationRequest $request)
{
// is allowed to?
$singleUserMode = FireflyConfig::get('single_user_mode', Config::get('firefly.configuration.single_user_mode'))->data;

View File

@@ -206,7 +206,7 @@ class BillController extends Controller
/** @var Carbon $date */
$date = session('start');
$year = $date->year;
$page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page'));
$page = intval($request->get('page'));
$pageSize = intval(Preferences::get('transactionPageSize', 50)->data);
$yearAverage = $repository->getYearAverage($bill, $date);
$overallAverage = $repository->getOverallAverage($bill);
@@ -217,7 +217,7 @@ class BillController extends Controller
$collector->setAllAssetAccounts()->setBills(new Collection([$bill]))->setLimit($pageSize)->setPage($page)->withBudgetInformation()
->withCategoryInformation();
$journals = $collector->getPaginatedJournals();
$journals->setPath('/bills/show/' . $bill->id);
$journals->setPath(route('bills.show', [$bill->id]));
$bill->nextExpectedMatch = $repository->nextExpectedMatch($bill, new Carbon);
$hideBill = true;

View File

@@ -293,37 +293,15 @@ class BudgetController extends Controller
);
}
$page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page'));
$page = intval($request->get('page'));
$pageSize = intval(Preferences::get('transactionPageSize', 50)->data);
$count = 0;
$loop = 0;
// grab journals, but be prepared to jump a period back to get the right ones:
Log::info('Now at no-budget loop start.');
while ($count === 0 && $loop < 3) {
$loop++;
Log::info(sprintf('Count is zero, search for journals between %s and %s.', $start->format('Y-m-d'), $end->format('Y-m-d')));
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setLimit($pageSize)->setPage($page)
->withoutBudget()->withOpposingAccount();
$journals = $collector->getPaginatedJournals();
$journals->setPath('/budgets/list/no-budget');
$count = $journals->getCollection()->count();
if ($count === 0 && $loop < 3) {
$start->subDay();
$start = Navigation::startOfPeriod($start, $range);
$end = Navigation::endOfPeriod($start, $range);
Log::info(sprintf('Count is still zero, go back in time to "%s" and "%s"!', $start->format('Y-m-d'), $end->format('Y-m-d')));
}
}
if ($moment !== 'all' && $loop > 1) {
$subTitle = trans(
'firefly.without_budget_between',
['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)]
);
}
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($start, $end)->setTypes([TransactionType::WITHDRAWAL])->setLimit($pageSize)->setPage($page)
->withoutBudget()->withOpposingAccount();
$journals = $collector->getPaginatedJournals();
$journals->setPath(route('budgets.no-budget'));
return view('budgets.no-budget', compact('journals', 'subTitle', 'moment', 'periods', 'start', 'end'));
}
@@ -357,7 +335,7 @@ class BudgetController extends Controller
/** @var Carbon $start */
$start = session('first', Carbon::create()->startOfYear());
$end = new Carbon;
$page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page'));
$page = intval($request->get('page'));
$pageSize = intval(Preferences::get('transactionPageSize', 50)->data);
$limits = $this->getLimits($budget, $start, $end);
$repetition = null;
@@ -366,7 +344,7 @@ class BudgetController extends Controller
$collector = app(JournalCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($start, $end)->setBudget($budget)->setLimit($pageSize)->setPage($page)->withCategoryInformation();
$journals = $collector->getPaginatedJournals();
$journals->setPath('/budgets/show/' . $budget->id);
$journals->setPath(route('budgets.show', [$budget->id]));
$subTitle = trans('firefly.all_journals_for_budget', ['name' => $budget->name]);
@@ -388,7 +366,7 @@ class BudgetController extends Controller
throw new FireflyException('This budget limit is not part of this budget.');
}
$page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page'));
$page = intval($request->get('page'));
$pageSize = intval(Preferences::get('transactionPageSize', 50)->data);
$subTitle = trans(
'firefly.budget_in_period', [
@@ -404,7 +382,7 @@ class BudgetController extends Controller
$collector->setAllAssetAccounts()->setRange($budgetLimit->start_date, $budgetLimit->end_date)
->setBudget($budget)->setLimit($pageSize)->setPage($page)->withCategoryInformation();
$journals = $collector->getPaginatedJournals();
$journals->setPath('/budgets/show/' . $budget->id . '/' . $budgetLimit->id);
$journals->setPath(route('budgets.show', [$budget->id, $budgetLimit->id]));
$start = session('first', Carbon::create()->startOfYear());
@@ -567,7 +545,7 @@ class BudgetController extends Controller
$start = $first->date ?? new Carbon;
$range = Preferences::get('viewRange', '1M')->data;
$start = Navigation::startOfPeriod($start, $range);
$end = Navigation::endOfX(new Carbon, $range);
$end = Navigation::endOfX(new Carbon, $range, null);
$entries = new Collection;
// properties for cache

View File

@@ -164,10 +164,12 @@ class CategoryController extends Controller
public function noCategory(Request $request, JournalRepositoryInterface $repository, string $moment = '')
{
// default values:
$range = Preferences::get('viewRange', '1M')->data;
$start = null;
$end = null;
$periods = new Collection;
$range = Preferences::get('viewRange', '1M')->data;
$start = null;
$end = null;
$periods = new Collection;
$page = intval($request->get('page'));
$pageSize = intval(Preferences::get('transactionPageSize', 50)->data);
// prep for "all" view.
if ($moment === 'all') {
@@ -199,37 +201,13 @@ class CategoryController extends Controller
);
}
$page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page'));
$pageSize = intval(Preferences::get('transactionPageSize', 50)->data);
$count = 0;
$loop = 0;
// grab journals, but be prepared to jump a period back to get the right ones:
Log::info('Now at no-cat loop start.');
while ($count === 0 && $loop < 3) {
$loop++;
Log::info('Count is zero, search for journals.');
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($start, $end)->setLimit($pageSize)->setPage($page)->withoutCategory()->withOpposingAccount();
$collector->removeFilter(InternalTransferFilter::class);
$journals = $collector->getPaginatedJournals();
$journals->setPath('/categories/list/no-category');
$count = $journals->getCollection()->count();
if ($count === 0 && $loop < 3) {
$start->subDay();
$start = Navigation::startOfPeriod($start, $range);
$end = Navigation::endOfPeriod($start, $range);
Log::info(sprintf('Count is still zero, go back in time to "%s" and "%s"!', $start->format('Y-m-d'), $end->format('Y-m-d')));
}
}
if ($moment !== 'all' && $loop > 1) {
$subTitle = trans(
'firefly.without_category_between',
['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)]
);
}
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($start, $end)->setLimit($pageSize)->setPage($page)->withoutCategory()->withOpposingAccount();
$collector->removeFilter(InternalTransferFilter::class);
$journals = $collector->getPaginatedJournals();
$journals->setPath(route('categories.no-category'));
return view('categories.no-category', compact('journals', 'subTitle', 'moment', 'periods', 'start', 'end'));
}
@@ -247,10 +225,8 @@ class CategoryController extends Controller
// default values:
$subTitle = $category->name;
$subTitleIcon = 'fa-bar-chart';
$page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page'));
$page = intval($request->get('page'));
$pageSize = intval(Preferences::get('transactionPageSize', 50)->data);
$count = 0;
$loop = 0;
$range = Preferences::get('viewRange', '1M')->data;
$start = null;
$end = null;
@@ -287,34 +263,15 @@ class CategoryController extends Controller
'end' => $end->formatLocalized($this->monthAndDayFormat)]
);
}
// grab journals, but be prepared to jump a period back to get the right ones:
Log::info('Now at category loop start.');
while ($count === 0 && $loop < 3) {
$loop++;
Log::info('Count is zero, search for journals.');
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($start, $end)->setLimit($pageSize)->setPage($page)->withOpposingAccount()
->setCategory($category)->withBudgetInformation()->withCategoryInformation();
$collector->removeFilter(InternalTransferFilter::class);
$journals = $collector->getPaginatedJournals();
$journals->setPath('categories/show/' . $category->id);
$count = $journals->getCollection()->count();
if ($count === 0 && $loop < 3) {
$start->subDay();
$start = Navigation::startOfPeriod($start, $range);
$end = Navigation::endOfPeriod($start, $range);
Log::info(sprintf('Count is still zero, go back in time to "%s" and "%s"!', $start->format('Y-m-d'), $end->format('Y-m-d')));
}
}
if ($moment !== 'all' && $loop > 1) {
$subTitle = trans(
'firefly.journals_in_period_for_category',
['name' => $category->name, 'start' => $start->formatLocalized($this->monthAndDayFormat),
'end' => $end->formatLocalized($this->monthAndDayFormat)]
);
}
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($start, $end)->setLimit($pageSize)->setPage($page)->withOpposingAccount()
->setCategory($category)->withBudgetInformation()->withCategoryInformation();
$collector->removeFilter(InternalTransferFilter::class);
$journals = $collector->getPaginatedJournals();
$journals->setPath(route('categories.show', [$category->id]));
return view('categories.show', compact('category', 'moment', 'journals', 'periods', 'subTitle', 'subTitleIcon', 'start', 'end'));
}
@@ -382,7 +339,7 @@ class CategoryController extends Controller
$start = $first->date ?? new Carbon;
$range = Preferences::get('viewRange', '1M')->data;
$start = Navigation::startOfPeriod($start, $range);
$end = Navigation::endOfX(new Carbon, $range);
$end = Navigation::endOfX(new Carbon, $range, null);
$entries = new Collection;
// properties for cache
@@ -468,7 +425,7 @@ class CategoryController extends Controller
}
$range = Preferences::get('viewRange', '1M')->data;
$first = Navigation::startOfPeriod($first, $range);
$end = Navigation::endOfX(new Carbon, $range);
$end = Navigation::endOfX(new Carbon, $range, null);
$entries = new Collection;
// properties for entries with their amounts.

View File

@@ -163,7 +163,7 @@ class BudgetController extends Controller
*
* @return \Illuminate\Http\JsonResponse
*/
public function expenseAsset(Budget $budget, BudgetLimit $budgetLimit = null)
public function expenseAsset(Budget $budget, ?BudgetLimit $budgetLimit)
{
$cache = new CacheProperties;
$cache->addProperty($budget->id);
@@ -208,7 +208,7 @@ class BudgetController extends Controller
*
* @return \Illuminate\Http\JsonResponse
*/
public function expenseCategory(Budget $budget, BudgetLimit $budgetLimit = null)
public function expenseCategory(Budget $budget, ?BudgetLimit $budgetLimit)
{
$cache = new CacheProperties;
$cache->addProperty($budget->id);
@@ -255,7 +255,7 @@ class BudgetController extends Controller
*
* @return \Illuminate\Http\JsonResponse
*/
public function expenseExpense(Budget $budget, BudgetLimit $budgetLimit = null)
public function expenseExpense(Budget $budget, ?BudgetLimit $budgetLimit)
{
$cache = new CacheProperties;
$cache->addProperty($budget->id);

View File

@@ -138,7 +138,7 @@ class HomeController extends Controller
}
return view(
'index', compact('count', 'title', 'subTitle', 'mainTitleIcon', 'transactions', 'showDepositsFrontpage', 'billCount')
'index', compact('count', 'subTitle', 'transactions', 'showDepositsFrontpage', 'billCount')
);
}

View File

@@ -0,0 +1,49 @@
<?php
/**
* BankController.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\Import;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Support\Import\Prerequisites\PrerequisitesInterface;
class BankController extends Controller
{
public function postPrerequisites()
{
}
/**
* @param string $bank
*/
public function prerequisites(string $bank)
{
$class = config(sprintf('firefly.import_pre.%s', $bank));
/** @var PrerequisitesInterface $object */
$object = app($class);
$object->setUser(auth()->user());
if ($object->hasPrerequisites()) {
$view = $object->getView();
$parameters = $object->getViewParameters();
return view($view, $parameters);
}
if (!$object->hasPrerequisites()) {
echo 'redirect to import form.';
}
}
}

View File

@@ -0,0 +1,306 @@
<?php
/**
* FileController.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\Import;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Http\Controllers\Controller;
use FireflyIII\Http\Requests\ImportUploadRequest;
use FireflyIII\Import\Configurator\ConfiguratorInterface;
use FireflyIII\Import\Routine\ImportRoutine;
use FireflyIII\Models\ImportJob;
use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface;
use FireflyIII\Repositories\Tag\TagRepositoryInterface;
use Illuminate\Http\Request;
use Illuminate\Http\Response as LaravelResponse;
use Log;
use Response;
use Session;
use View;
/**
* Class FileController.
*
* @package FireflyIII\Http\Controllers\Import
*/
class FileController extends Controller
{
/** @var ImportJobRepositoryInterface */
public $repository;
/**
*
*/
public function __construct()
{
parent::__construct();
$this->middleware(
function ($request, $next) {
View::share('mainTitleIcon', 'fa-archive');
View::share('title', trans('firefly.import_index_title'));
$this->repository = app(ImportJobRepositoryInterface::class);
return $next($request);
}
);
}
/**
* This is step 3. This repeats until the job is configured.
*
* @param ImportJob $job
*
* @return View
* @throws FireflyException
*/
public function configure(ImportJob $job)
{
// create configuration class:
$configurator = $this->makeConfigurator($job);
// is the job already configured?
if ($configurator->isJobConfigured()) {
$this->repository->updateStatus($job, 'configured');
return redirect(route('import.file.status', [$job->key]));
}
$view = $configurator->getNextView();
$data = $configurator->getNextData();
$subTitle = trans('firefly.import_config_bread_crumb');
$subTitleIcon = 'fa-wrench';
return view($view, compact('data', 'job', 'subTitle', 'subTitleIcon'));
}
/**
* Generate a JSON file of the job's configuration and send it to the user.
*
* @param ImportJob $job
*
* @return LaravelResponse
*/
public function download(ImportJob $job)
{
Log::debug('Now in download()', ['job' => $job->key]);
$config = $job->configuration;
// This is CSV import specific:
$config['column-roles-complete'] = false;
$config['column-mapping-complete'] = false;
$config['initial-config-complete'] = false;
$config['delimiter'] = $config['delimiter'] === "\t" ? 'tab' : $config['delimiter'];
$result = json_encode($config, JSON_PRETTY_PRINT);
$name = sprintf('"%s"', addcslashes('import-configuration-' . date('Y-m-d') . '.json', '"\\'));
/** @var LaravelResponse $response */
$response = response($result, 200);
$response->header('Content-disposition', 'attachment; filename=' . $name)
->header('Content-Type', 'application/json')
->header('Content-Description', 'File Transfer')
->header('Connection', 'Keep-Alive')
->header('Expires', '0')
->header('Cache-Control', 'must-revalidate, post-check=0, pre-check=0')
->header('Pragma', 'public')
->header('Content-Length', strlen($result));
return $response;
}
/**
* This is step 1. Upload a file.
*
* @return View
*/
public function index()
{
$subTitle = trans('firefly.import_index_sub_title');
$subTitleIcon = 'fa-home';
$importFileTypes = [];
$defaultImportType = config('firefly.default_import_format');
foreach (array_keys(config('firefly.import_formats')) as $type) {
$importFileTypes[$type] = trans('firefly.import_file_type_' . $type);
}
return view('import.file.index', compact('subTitle', 'subTitleIcon', 'importFileTypes', 'defaultImportType'));
}
/**
* This is step 2. It creates an Import Job. Stores the import.
*
* @param ImportUploadRequest $request
*
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function initialize(ImportUploadRequest $request)
{
Log::debug('Now in initialize()');
// create import job:
$type = $request->get('import_file_type');
$job = $this->repository->create($type);
Log::debug('Created new job', ['key' => $job->key, 'id' => $job->id]);
// process file:
$this->repository->processFile($job, $request->files->get('import_file'));
// process config, if present:
if ($request->files->has('configuration_file')) {
$this->repository->processConfiguration($job, $request->files->get('configuration_file'));
}
$this->repository->updateStatus($job, 'initialized');
return redirect(route('import.file.configure', [$job->key]));
}
/**
*
* Show status of import job in JSON.
*
* @param ImportJob $job
*
* @return \Illuminate\Http\JsonResponse
*/
public function json(ImportJob $job)
{
$result = [
'started' => false,
'finished' => false,
'running' => false,
'errors' => array_values($job->extended_status['errors']),
'percentage' => 0,
'show_percentage' => false,
'steps' => $job->extended_status['steps'],
'done' => $job->extended_status['done'],
'statusText' => trans('firefly.import_status_job_' . $job->status),
'status' => $job->status,
'finishedText' => '',
];
if ($job->extended_status['steps'] !== 0) {
$result['percentage'] = round(($job->extended_status['done'] / $job->extended_status['steps']) * 100, 0);
$result['show_percentage'] = true;
}
if ($job->status === 'finished') {
$tagId = $job->extended_status['tag'];
/** @var TagRepositoryInterface $repository */
$repository = app(TagRepositoryInterface::class);
$tag = $repository->find($tagId);
$result['finished'] = true;
$result['finishedText'] = trans('firefly.import_status_finished_job', ['link' => route('tags.show', [$tag->id, 'all']), 'tag' => $tag->tag]);
}
if ($job->status === 'running') {
$result['started'] = true;
$result['running'] = true;
}
return Response::json($result);
}
/**
* Step 4. Save the configuration.
*
* @param Request $request
* @param ImportJob $job
*
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function postConfigure(Request $request, ImportJob $job)
{
Log::debug('Now in postConfigure()', ['job' => $job->key]);
$configurator = $this->makeConfigurator($job);
// is the job already configured?
if ($configurator->isJobConfigured()) {
return redirect(route('import.file.status', [$job->key]));
}
$data = $request->all();
$configurator->configureJob($data);
// get possible warning from configurator:
$warning = $configurator->getWarningMessage();
if (strlen($warning) > 0) {
Session::flash('warning', $warning);
}
// return to configure
return redirect(route('import.file.configure', [$job->key]));
}
/**
* @param ImportJob $job
*
* @return \Illuminate\Http\JsonResponse
* @throws FireflyException
*/
public function start(ImportJob $job)
{
/** @var ImportRoutine $routine */
$routine = app(ImportRoutine::class);
$routine->setJob($job);
$result = $routine->run();
if ($result) {
return Response::json(['run' => 'ok']);
}
throw new FireflyException('Job did not complete succesfully.');
}
/**
* @param ImportJob $job
*
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|View
*/
public function status(ImportJob $job)
{
$statuses = ['configured', 'running', 'finished'];
if (!in_array($job->status, $statuses)) {
return redirect(route('import.file.configure', [$job->key]));
}
$subTitle = trans('firefly.import_status_sub_title');
$subTitleIcon = 'fa-star';
return view('import.file.status', compact('job', 'subTitle', 'subTitleIcon'));
}
/**
* @param ImportJob $job
*
* @return ConfiguratorInterface
* @throws FireflyException
*/
private function makeConfigurator(ImportJob $job): ConfiguratorInterface
{
$type = $job->file_type;
$key = sprintf('firefly.import_configurators.%s', $type);
$className = config($key);
if (is_null($className)) {
throw new FireflyException('Cannot find configurator class for this job.'); // @codeCoverageIgnore
}
/** @var ConfiguratorInterface $configurator */
$configurator = app($className);
$configurator->setJob($job);
return $configurator;
}
}

View File

@@ -12,17 +12,7 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Http\Requests\ImportUploadRequest;
use FireflyIII\Import\Configurator\ConfiguratorInterface;
use FireflyIII\Import\Routine\ImportRoutine;
use FireflyIII\Models\ImportJob;
use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface;
use FireflyIII\Repositories\Tag\TagRepositoryInterface;
use Illuminate\Http\Request;
use Illuminate\Http\Response as LaravelResponse;
use Log;
use Response;
use View;
/**
@@ -53,72 +43,9 @@ class ImportController extends Controller
);
}
/**
* This is step 3. This repeats until the job is configured.
*
* @param ImportJob $job
*
* @return View
* @throws FireflyException
*/
public function configure(ImportJob $job)
{
// create configuration class:
$configurator = $this->makeConfigurator($job);
// is the job already configured?
if ($configurator->isJobConfigured()) {
$this->repository->updateStatus($job, 'configured');
return redirect(route('import.status', [$job->key]));
}
$view = $configurator->getNextView();
$data = $configurator->getNextData();
$subTitle = trans('firefly.import_config_bread_crumb');
$subTitleIcon = 'fa-wrench';
return view($view, compact('data', 'job', 'subTitle', 'subTitleIcon'));
}
/**
* Generate a JSON file of the job's configuration and send it to the user.
*
* @param ImportJob $job
*
* @return LaravelResponse
*/
public function download(ImportJob $job)
{
Log::debug('Now in download()', ['job' => $job->key]);
$config = $job->configuration;
// This is CSV import specific:
$config['column-roles-complete'] = false;
$config['column-mapping-complete'] = false;
$config['initial-config-complete'] = false;
$config['delimiter'] = $config['delimiter'] === "\t" ? 'tab' : $config['delimiter'];
$result = json_encode($config, JSON_PRETTY_PRINT);
$name = sprintf('"%s"', addcslashes('import-configuration-' . date('Y-m-d') . '.json', '"\\'));
/** @var LaravelResponse $response */
$response = response($result, 200);
$response->header('Content-disposition', 'attachment; filename=' . $name)
->header('Content-Type', 'application/json')
->header('Content-Description', 'File Transfer')
->header('Connection', 'Keep-Alive')
->header('Expires', '0')
->header('Cache-Control', 'must-revalidate, post-check=0, pre-check=0')
->header('Pragma', 'public')
->header('Content-Length', strlen($result));
return $response;
}
/**
* This is step 1. Upload a file.
* General import index
*
* @return View
*/
@@ -136,160 +63,4 @@ class ImportController extends Controller
return view('import.index', compact('subTitle', 'subTitleIcon', 'importFileTypes', 'defaultImportType'));
}
/**
* This is step 2. It creates an Import Job. Stores the import.
*
* @param ImportUploadRequest $request
*
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function initialize(ImportUploadRequest $request)
{
Log::debug('Now in initialize()');
// create import job:
$type = $request->get('import_file_type');
$job = $this->repository->create($type);
Log::debug('Created new job', ['key' => $job->key, 'id' => $job->id]);
// process file:
$this->repository->processFile($job, $request->files->get('import_file'));
// process config, if present:
if ($request->files->has('configuration_file')) {
$this->repository->processConfiguration($job, $request->files->get('configuration_file'));
}
$this->repository->updateStatus($job, 'initialized');
return redirect(route('import.configure', [$job->key]));
}
/**
*
* Show status of import job in JSON.
*
* @param ImportJob $job
*
* @return \Illuminate\Http\JsonResponse
*/
public function json(ImportJob $job)
{
$result = [
'started' => false,
'finished' => false,
'running' => false,
'errors' => array_values($job->extended_status['errors']),
'percentage' => 0,
'show_percentage' => false,
'steps' => $job->extended_status['steps'],
'done' => $job->extended_status['done'],
'statusText' => trans('firefly.import_status_job_' . $job->status),
'status' => $job->status,
'finishedText' => '',
];
if ($job->extended_status['steps'] !== 0) {
$result['percentage'] = round(($job->extended_status['done'] / $job->extended_status['steps']) * 100, 0);
$result['show_percentage'] = true;
}
if ($job->status === 'finished') {
$tagId = $job->extended_status['tag'];
/** @var TagRepositoryInterface $repository */
$repository = app(TagRepositoryInterface::class);
$tag = $repository->find($tagId);
$result['finished'] = true;
$result['finishedText'] = trans('firefly.import_status_finished_job', ['link' => route('tags.show', [$tag->id, 'all']), 'tag' => $tag->tag]);
}
if ($job->status === 'running') {
$result['started'] = true;
$result['running'] = true;
}
return Response::json($result);
}
/**
* Step 4. Save the configuration.
*
* @param Request $request
* @param ImportJob $job
*
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector
*/
public function postConfigure(Request $request, ImportJob $job)
{
Log::debug('Now in postConfigure()', ['job' => $job->key]);
$configurator = $this->makeConfigurator($job);
// is the job already configured?
if ($configurator->isJobConfigured()) {
return redirect(route('import.status', [$job->key]));
}
$data = $request->all();
$configurator->configureJob($data);
// return to configure
return redirect(route('import.configure', [$job->key]));
}
/**
* @param ImportJob $job
*
* @return \Illuminate\Http\JsonResponse
* @throws FireflyException
*/
public function start(ImportJob $job)
{
/** @var ImportRoutine $routine */
$routine = app(ImportRoutine::class);
$routine->setJob($job);
$result = $routine->run();
if ($result) {
return Response::json(['run' => 'ok']);
}
throw new FireflyException('Job did not complete succesfully.');
}
/**
* @param ImportJob $job
*
* @return \Illuminate\Http\RedirectResponse|\Illuminate\Routing\Redirector|View
*/
public function status(ImportJob $job)
{
$statuses = ['configured', 'running', 'finished'];
if (!in_array($job->status, $statuses)) {
return redirect(route('import.configure', [$job->key]));
}
$subTitle = trans('firefly.import_status_sub_title');
$subTitleIcon = 'fa-star';
return view('import.status', compact('job', 'subTitle', 'subTitleIcon'));
}
/**
* @param ImportJob $job
*
* @return ConfiguratorInterface
* @throws FireflyException
*/
private function makeConfigurator(ImportJob $job): ConfiguratorInterface
{
$type = $job->file_type;
$key = sprintf('firefly.import_configurators.%s', $type);
$className = config($key);
if (is_null($className)) {
throw new FireflyException('Cannot find configurator class for this job.'); // @codeCoverageIgnore
}
/** @var ConfiguratorInterface $configurator */
$configurator = app($className);
$configurator->setJob($job);
return $configurator;
}
}

View File

@@ -12,6 +12,7 @@ declare(strict_types=1);
namespace FireflyIII\Http\Controllers;
use Amount;
use Carbon\Carbon;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType;
@@ -19,9 +20,9 @@ use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Repositories\Account\AccountRepositoryInterface;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use Illuminate\Http\Request;
use Log;
use Navigation;
use Preferences;
use Session;
/**
* Class JavascriptController
@@ -88,11 +89,6 @@ class JavascriptController extends Controller
*/
public function variables(Request $request)
{
$picker = $this->getDateRangePicker();
$start = Session::get('start');
$end = Session::get('end');
$linkTitle = sprintf('%s - %s', $start->formatLocalized($this->monthAndDayFormat), $end->formatLocalized($this->monthAndDayFormat));
$firstDate = session('first')->format('Y-m-d');
$localeconv = localeconv();
$accounting = Amount::getJsConfig($localeconv);
$localeconv = localeconv();
@@ -100,15 +96,16 @@ class JavascriptController extends Controller
$localeconv['frac_digits'] = $defaultCurrency->decimal_places;
$pref = Preferences::get('language', config('firefly.default_language', 'en_US'));
$lang = $pref->data;
$data = [
'picker' => $picker,
'linkTitle' => $linkTitle,
'firstDate' => $firstDate,
'currencyCode' => Amount::getCurrencyCode(),
'currencySymbol' => Amount::getCurrencySymbol(),
'accounting' => $accounting,
'localeconv' => $localeconv,
'language' => $lang,
$dateRange = $this->getDateRangeConfig();
$data = [
'currencyCode' => Amount::getCurrencyCode(),
'currencySymbol' => Amount::getCurrencySymbol(),
'accounting' => $accounting,
'localeconv' => $localeconv,
'language' => $lang,
'dateRangeTitle' => $dateRange['title'],
'dateRangeConfig' => $dateRange['configuration'],
];
$request->session()->keep(['two-factor-secret']);
@@ -119,40 +116,73 @@ class JavascriptController extends Controller
/**
* @return array
* @throws FireflyException
*/
private function getDateRangePicker(): array
private function getDateRangeConfig(): array
{
$viewRange = Preferences::get('viewRange', '1M')->data;
$start = Session::get('start');
$end = Session::get('end');
$start = session('start');
$end = session('end');
$first = session('first');
$title = sprintf('%s - %s', $start->formatLocalized($this->monthAndDayFormat), $end->formatLocalized($this->monthAndDayFormat));
$isCustom = session('is_custom_range');
$ranges = [
// first range is the current range:
$title => [$start, $end],
];
Log::debug(sprintf('viewRange is %s', $viewRange));
$prevStart = clone $start;
$prevEnd = clone $start;
$nextStart = clone $end;
$nextEnd = clone $end;
if ($viewRange === 'custom') {
$days = $start->diffInDays($end);
$prevStart->subDays($days);
$nextEnd->addDays($days);
unset($days);
// get the format for the ranges:
$format = $this->getFormatByRange($viewRange);
// when current range is a custom range, add the current period as the next range.
if ($isCustom) {
Log::debug('Custom is true.');
$index = $start->formatLocalized($format);
$customPeriodStart = Navigation::startOfPeriod($start, $viewRange);
$customPeriodEnd = Navigation::endOfPeriod($customPeriodStart, $viewRange);
$ranges[$index] = [$customPeriodStart, $customPeriodEnd];
}
// then add previous range and next range
$previousDate = Navigation::subtractPeriod($start, $viewRange);
$index = $previousDate->formatLocalized($format);
$previousStart = Navigation::startOfPeriod($previousDate, $viewRange);
$previousEnd = Navigation::endOfPeriod($previousStart, $viewRange);
$ranges[$index] = [$previousStart, $previousEnd];
if ($viewRange !== 'custom') {
$prevStart = Navigation::subtractPeriod($start, $viewRange);// subtract for previous period
$prevEnd = Navigation::endOfPeriod($prevStart, $viewRange);
$nextStart = Navigation::addPeriod($start, $viewRange, 0); // add for previous period
$nextEnd = Navigation::endOfPeriod($nextStart, $viewRange);
}
$nextDate = Navigation::addPeriod($start, $viewRange, 0);
$index = $nextDate->formatLocalized($format);
$nextStart = Navigation::startOfPeriod($nextDate, $viewRange);
$nextEnd = Navigation::endOfPeriod($nextStart, $viewRange);
$ranges[$index] = [$nextStart, $nextEnd];
$ranges = [];
$ranges['current'] = [$start->format('Y-m-d'), $end->format('Y-m-d')];
$ranges['previous'] = [$prevStart->format('Y-m-d'), $prevEnd->format('Y-m-d')];
$ranges['next'] = [$nextStart->format('Y-m-d'), $nextEnd->format('Y-m-d')];
// everything
$index = strval(trans('firefly.everything'));
$ranges[$index] = [$first, new Carbon];
$return = [
'title' => $title,
'configuration' => [
'apply' => strval(trans('firefly.apply')),
'cancel' => strval(trans('firefly.cancel')),
'from' => strval(trans('firefly.from')),
'to' => strval(trans('firefly.to')),
'customRange' => strval(trans('firefly.customRange')),
'start' => $start->format('Y-m-d'),
'end' => $end->format('Y-m-d'),
'ranges' => $ranges,
],
];
return $return;
}
private function getFormatByRange(string $viewRange): string
{
switch ($viewRange) {
default:
throw new FireflyException('The date picker does not yet support "' . $viewRange . '".'); // @codeCoverageIgnore
throw new FireflyException(sprintf('The date picker does not yet support "%s".', $viewRange)); // @codeCoverageIgnore
case '1D':
case 'custom':
$format = (string)trans('config.month_and_day');
@@ -174,18 +204,6 @@ class JavascriptController extends Controller
break;
}
$current = $start->formatLocalized($format);
$next = $nextStart->formatLocalized($format);
$prev = $prevStart->formatLocalized($format);
return [
'start' => $start->format('Y-m-d'),
'end' => $end->format('Y-m-d'),
'current' => $current,
'previous' => $prev,
'next' => $next,
'ranges' => $ranges,
];
return $format;
}
}

View File

@@ -163,4 +163,4 @@ class IntroController
return $steps;
}
}
}

View File

@@ -22,7 +22,6 @@ use FireflyIII\Repositories\Tag\TagRepositoryInterface;
use FireflyIII\Support\CacheProperties;
use Illuminate\Http\Request;
use Illuminate\Support\Collection;
use Log;
use Navigation;
use Preferences;
use Session;
@@ -79,22 +78,16 @@ class TagController extends Controller
}
/**
* @param Request $request
* Create a new tag.
*
* @return View
*/
public function create(Request $request)
public function create()
{
$subTitle = trans('firefly.new_tag');
$subTitleIcon = 'fa-tag';
$apiKey = env('GOOGLE_MAPS_API_KEY', '');
$preFilled = [
'tagMode' => 'nothing',
];
if (!$request->old('tagMode')) {
Session::flash('preFilled', $preFilled);
}
// put previous url in session if not redirect from store (not "create another").
if (session('tags.create.fromStore') !== true) {
$this->rememberPreviousUri('tags.create.uri');
@@ -107,6 +100,8 @@ class TagController extends Controller
}
/**
* Delete a tag
*
* @param Tag $tag
*
* @return View
@@ -141,38 +136,18 @@ class TagController extends Controller
}
/**
* @param Tag $tag
* Edit a tag
*
* @param TagRepositoryInterface $repository
* @param Tag $tag
*
* @return View
*/
public function edit(Tag $tag, TagRepositoryInterface $repository)
public function edit(Tag $tag)
{
$subTitle = trans('firefly.edit_tag', ['tag' => $tag->tag]);
$subTitleIcon = 'fa-tag';
$apiKey = env('GOOGLE_MAPS_API_KEY', '');
/*
* Default tag options (again)
*/
$tagOptions = $this->tagOptions;
/*
* Can this tag become another type?
*/
$allowAdvance = $repository->tagAllowAdvance($tag);
$allowToBalancingAct = $repository->tagAllowBalancing($tag);
// edit tag options:
if ($allowAdvance === false) {
unset($tagOptions['advancePayment']);
}
if ($allowToBalancingAct === false) {
unset($tagOptions['balancingAct']);
}
// put previous url in session if not redirect from store (not "return_to_edit").
if (session('tags.edit.fromUpdate') !== true) {
$this->rememberPreviousUri('tags.edit.uri');
@@ -181,10 +156,12 @@ class TagController extends Controller
Session::flash('gaEventCategory', 'tags');
Session::flash('gaEventAction', 'edit');
return view('tags.edit', compact('tag', 'subTitle', 'subTitleIcon', 'tagOptions', 'apiKey'));
return view('tags.edit', compact('tag', 'subTitle', 'subTitleIcon', 'apiKey'));
}
/**
* View all tags
*
* @param TagRepositoryInterface $repository
*
* @return View
@@ -194,6 +171,8 @@ class TagController extends Controller
$title = 'Tags';
$mainTitleIcon = 'fa-tags';
$types = ['nothing', 'balancingAct', 'advancePayment'];
$hasTypes = 0; // which types of tag the user actually has.
$counts = []; // how many of each type?
$count = $repository->count();
// loop each types and get the tags, group them by year.
@@ -209,10 +188,13 @@ class TagController extends Controller
return strtolower($date . $tag->tag);
}
);
if ($tags->count() > 0) {
$hasTypes++;
}
$counts[$type] = $tags->count();
/** @var Tag $tag */
foreach ($tags as $tag) {
$year = is_null($tag->date) ? trans('firefly.no_year') : $tag->date->year;
$monthFormatted = is_null($tag->date) ? trans('firefly.no_month') : $tag->date->formatLocalized($this->monthFormat);
@@ -220,7 +202,7 @@ class TagController extends Controller
}
}
return view('tags.index', compact('title', 'mainTitleIcon', 'types', 'collection', 'count'));
return view('tags.index', compact('title', 'mainTitleIcon', 'counts', 'hasTypes', 'types', 'collection', 'count'));
}
/**
@@ -236,17 +218,15 @@ class TagController extends Controller
// default values:
$subTitle = $tag->tag;
$subTitleIcon = 'fa-tag';
$page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page'));
$page = intval($request->get('page'));
$pageSize = intval(Preferences::get('transactionPageSize', 50)->data);
$count = 0;
$loop = 0;
$range = Preferences::get('viewRange', '1M')->data;
$start = null;
$end = null;
$periods = new Collection;
$apiKey = env('GOOGLE_MAPS_API_KEY', '');
$sum = '0';
$path = 'tags/show/' . $tag->id;
$path = route('tags.show', [$tag->id]);
// prep for "all" view.
@@ -254,8 +234,8 @@ 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);
$path = 'tags/show/' . $tag->id . '/all';
$sum = $repository->sumOfTag($tag, null, null);
$path = route('tags.show', [$tag->id, 'all']);
}
// prep for "specific date" view.
@@ -269,6 +249,7 @@ class TagController extends Controller
);
$periods = $this->getPeriodOverview($tag);
$sum = $repository->sumOfTag($tag, $start, $end);
$path = route('tags.show', [$tag->id, $moment]);
}
// prep for current period
@@ -281,32 +262,14 @@ class TagController extends Controller
['tag' => $tag->tag, 'start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)]
);
}
// grab journals, but be prepared to jump a period back to get the right ones:
Log::info('Now at tag loop start.');
while ($count === 0 && $loop < 3) {
$loop++;
Log::info(sprintf('Count is zero, search for journals between %s and %s (pagesize %d, page %d).', $start, $end, $pageSize, $page));
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($start, $end)->setLimit($pageSize)->setPage($page)->withOpposingAccount()
->setTag($tag)->withBudgetInformation()->withCategoryInformation()->removeFilter(InternalTransferFilter::class);
$journals = $collector->getPaginatedJournals();
$journals->setPath($path);
$count = $journals->getCollection()->count();
if ($count === 0 && $loop < 3) {
$start->subDay();
$start = Navigation::startOfPeriod($start, $range);
$end = Navigation::endOfPeriod($start, $range);
Log::info(sprintf('Count is still zero, go back in time to "%s" and "%s"!', $start->format('Y-m-d'), $end->format('Y-m-d')));
}
}
if ($moment !== 'all' && $loop > 1) {
$subTitle = trans(
'firefly.journals_in_period_for_tag',
['tag' => $tag->tag, 'start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)]
);
}
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($start, $end)->setLimit($pageSize)->setPage($page)->withOpposingAccount()
->setTag($tag)->withBudgetInformation()->withCategoryInformation()->removeFilter(InternalTransferFilter::class);
$journals = $collector->getPaginatedJournals();
$journals->setPath($path);
return view('tags.show', compact('apiKey', 'tag', 'periods', 'subTitle', 'subTitleIcon', 'journals', 'sum', 'start', 'end', 'moment'));
}

View File

@@ -86,7 +86,7 @@ class MassController extends Controller
foreach ($ids as $journalId) {
/** @var TransactionJournal $journal */
$journal = $repository->find(intval($journalId));
if (!is_null($journal->id) && $journalId === $journal->id) {
if (!is_null($journal->id) && intval($journalId) === $journal->id) {
$set->push($journal);
}
}
@@ -131,10 +131,9 @@ class MassController extends Controller
$filtered = new Collection;
$messages = [];
/**
* @var int $index
* @var TransactionJournal $journal
*/
foreach ($journals as $index => $journal) {
foreach ($journals as $journal) {
$sources = $journal->sourceAccountList();
$destinations = $journal->destinationAccountList();
if ($sources->count() > 1) {

View File

@@ -199,10 +199,10 @@ class SingleController extends Controller
}
/**
* @param JournalRepositoryInterface $repository
* @param TransactionJournal $transactionJournal
* @param TransactionJournal $transactionJournal
*
* @return \Illuminate\Http\RedirectResponse
* @internal param JournalRepositoryInterface $repository
*/
public function destroy(TransactionJournal $transactionJournal)
{

View File

@@ -114,7 +114,7 @@ class SplitController extends Controller
'transactions.split.edit',
compact(
'subTitleIcon', 'currencies', 'optionalFields',
'preFilled', 'subTitle', 'amount', 'sourceAccounts', 'uploadSize', 'destinationAccounts', 'assetAccounts',
'preFilled', 'subTitle', 'uploadSize', 'assetAccounts',
'budgets', 'journal'
)
);

View File

@@ -70,15 +70,13 @@ class TransactionController extends Controller
// default values:
$subTitleIcon = config('firefly.transactionIconsByWhat.' . $what);
$types = config('firefly.transactionTypesByWhat.' . $what);
$page = intval($request->get('page')) === 0 ? 1 : intval($request->get('page'));
$page = intval($request->get('page'));
$pageSize = intval(Preferences::get('transactionPageSize', 50)->data);
$count = 0;
$loop = 0;
$range = Preferences::get('viewRange', '1M')->data;
$start = null;
$end = null;
$periods = new Collection;
$path = '/transactions/' . $what;
$path = route('transactions.index', [$what]);
// prep for "all" view.
if ($moment === 'all') {
@@ -86,14 +84,14 @@ class TransactionController extends Controller
$first = $repository->first();
$start = $first->date ?? new Carbon;
$end = new Carbon;
$path = '/transactions/' . $what . '/all/';
$path = route('transactions.index', [$what, 'all']);
}
// prep for "specific date" view.
if (strlen($moment) > 0 && $moment !== 'all') {
$start = new Carbon($moment);
$end = Navigation::endOfPeriod($start, $range);
$path = '/transactions/' . $what . '/' . $moment;
$path = route('transactions.index', [$what, $moment]);
$subTitle = trans(
'firefly.title_' . $what . '_between',
['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)]
@@ -111,32 +109,14 @@ class TransactionController extends Controller
['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)]
);
}
// grab journals, but be prepared to jump a period back to get the right ones:
Log::info('Now at transaction loop start.');
while ($count === 0 && $loop < 3) {
$loop++;
Log::info('Count is zero, search for journals.');
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($start, $end)->setTypes($types)->setLimit($pageSize)->setPage($page)->withOpposingAccount();
$collector->removeFilter(InternalTransferFilter::class);
$journals = $collector->getPaginatedJournals();
$journals->setPath($path);
$count = $journals->getCollection()->count();
if ($count === 0 && $loop < 3) {
$start->subDay();
$start = Navigation::startOfPeriod($start, $range);
$end = Navigation::endOfPeriod($start, $range);
Log::info(sprintf('Count is still zero, go back in time to "%s" and "%s"!', $start->format('Y-m-d'), $end->format('Y-m-d')));
}
}
if ($moment !== 'all' && $loop > 1) {
$subTitle = trans(
'firefly.title_' . $what . '_between',
['start' => $start->formatLocalized($this->monthAndDayFormat), 'end' => $end->formatLocalized($this->monthAndDayFormat)]
);
}
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
$collector->setAllAssetAccounts()->setRange($start, $end)->setTypes($types)->setLimit($pageSize)->setPage($page)->withOpposingAccount();
$collector->removeFilter(InternalTransferFilter::class);
$journals = $collector->getPaginatedJournals();
$journals->setPath($path);
return view('transactions.index', compact('subTitle', 'what', 'subTitleIcon', 'journals', 'periods', 'start', 'end', 'moment'));
@@ -204,7 +184,7 @@ class TransactionController extends Controller
$start = $first->date ?? new Carbon;
$range = Preferences::get('viewRange', '1M')->data;
$start = Navigation::startOfPeriod($start, $range);
$end = Navigation::endOfX(new Carbon, $range);
$end = Navigation::endOfX(new Carbon, $range, null);
$entries = new Collection;
$types = config('firefly.transactionTypesByWhat.' . $what);

View File

@@ -37,7 +37,7 @@ class ProfileFormRequest extends Request
{
return [
'current_password' => 'required',
'new_password' => 'required|confirmed',
'new_password' => 'required|confirmed|secure_password',
'new_password_confirmation' => 'required',
];
}

View File

@@ -54,7 +54,6 @@ class TagFormRequest extends Request
'latitude' => $latitude,
'longitude' => $longitude,
'zoomLevel' => $zoomLevel,
'tagMode' => $this->string('tagMode'),
];
return $data;
@@ -84,7 +83,6 @@ class TagFormRequest extends Request
'latitude' => 'numeric|min:-90|max:90',
'longitude' => 'numeric|min:-90|max:90',
'zoomLevel' => 'numeric|min:0|max:80',
'tagMode' => 'required|in:nothing,balancingAct,advancePayment',
];
}
}

View File

@@ -51,7 +51,7 @@ class UserFormRequest extends Request
return [
'id' => 'required|exists:users,id',
'email' => 'email|required',
'password' => 'confirmed',
'password' => 'confirmed|secure_password',
'blocked_code' => 'between:0,30',
'blocked' => 'between:0,1|numeric',
];

View File

@@ -0,0 +1,42 @@
<?php
/**
* UserRegistrationRequest.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\Requests;
/**
* Class UserRegistrationRequest
*
*
* @package FireflyIII\Http\Requests
*/
class UserRegistrationRequest extends Request
{
/**
* @return bool
*/
public function authorize()
{
// Only everybody
return true;
}
/**
* @return array
*/
public function rules()
{
return [
'email' => 'email|required',
'password' => 'confirmed|secure_password',
];
}
}

View File

@@ -471,16 +471,26 @@ Breadcrumbs::register(
}
);
/**
* FILE IMPORT
*/
Breadcrumbs::register(
'import.configure', function (BreadCrumbGenerator $breadcrumbs, ImportJob $job) {
'import.file.index', function (BreadCrumbGenerator $breadcrumbs) {
$breadcrumbs->parent('import.index');
$breadcrumbs->push(trans('firefly.import_config_sub_title', ['key' => $job->key]), route('import.configure', [$job->key]));
$breadcrumbs->push(trans('firefly.import_file'), route('import.file.index'));
}
);
Breadcrumbs::register(
'import.file.configure', function (BreadCrumbGenerator $breadcrumbs, ImportJob $job) {
$breadcrumbs->parent('import.file.index');
$breadcrumbs->push(trans('firefly.import_config_sub_title', ['key' => $job->key]), route('import.file.configure', [$job->key]));
}
);
Breadcrumbs::register(
'import.status', function (BreadCrumbGenerator $breadcrumbs, ImportJob $job) {
$breadcrumbs->parent('import.index');
$breadcrumbs->push(trans('firefly.import_status_bread_crumb', ['key' => $job->key]), route('import.status', [$job->key]));
'import.file.status', function (BreadCrumbGenerator $breadcrumbs, ImportJob $job) {
$breadcrumbs->parent('import.file.index');
$breadcrumbs->push(trans('firefly.import_status_bread_crumb', ['key' => $job->key]), route('import.file.status', [$job->key]));
}
);
@@ -805,7 +815,7 @@ Breadcrumbs::register(
* MASS TRANSACTION EDIT / DELETE
*/
Breadcrumbs::register(
'transactions.mass.edit', function (BreadCrumbGenerator $breadcrumbs, Collection $journals) {
'transactions.mass.edit', function (BreadCrumbGenerator $breadcrumbs, Collection $journals): void {
if ($journals->count() > 0) {
$journalIds = $journals->pluck('id')->toArray();
@@ -817,6 +827,8 @@ Breadcrumbs::register(
}
$breadcrumbs->parent('index');
return;
}
);

View File

@@ -48,6 +48,13 @@ interface ConfiguratorInterface
*/
public function getNextView(): string;
/**
* Return possible warning to user.
*
* @return string
*/
public function getWarningMessage(): string;
/**
* Returns true when the initial configuration for this job is complete.
*

View File

@@ -26,8 +26,12 @@ use Log;
*/
class CsvConfigurator implements ConfiguratorInterface
{
/** @var ImportJob */
private $job;
/** @var string */
private $warning = '';
/**
* ConfiguratorInterface constructor.
*/
@@ -50,8 +54,10 @@ class CsvConfigurator implements ConfiguratorInterface
/** @var ConfigurationInterface $object */
$object = new $class($this->job);
$object->setJob($job);
$result = $object->storeConfiguration($data);
$this->warning = $object->getWarningMessage();
return $object->storeConfiguration($data);
return $result;
}
/**
@@ -91,6 +97,16 @@ class CsvConfigurator implements ConfiguratorInterface
throw new FireflyException('No view for state');
}
/**
* Return possible warning to user.
*
* @return string
*/
public function getWarningMessage(): string
{
return $this->warning;
}
/**
* @return bool
*/

View File

@@ -29,6 +29,8 @@ class Amount implements ConverterInterface
* @param $value
*
* @return string
*
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
*/
public function convert($value): string
{

View File

@@ -58,7 +58,7 @@ class CsvProcessor implements FileProcessorInterface
}
/**
* Does the actual job:
* Does the actual job.
*
* @return bool
*/
@@ -66,34 +66,46 @@ class CsvProcessor implements FileProcessorInterface
{
Log::debug('Now in CsvProcessor run(). Job is now running...');
$entries = $this->getImportArray();
$index = 0;
$entries = new Collection($this->getImportArray());
Log::notice('Building importable objects from CSV file.');
foreach ($entries as $index => $row) {
// verify if not exists already:
if ($this->rowAlreadyImported($row)) {
$message = sprintf('Row #%d has already been imported.', $index);
$this->job->addError($index, $message);
$this->job->addStepsDone(5); // all steps.
Log::info($message);
continue;
}
$this->objects->push($this->importRow($index, $row));
$this->job->addStepsDone(1);
}
// if job has no step count, set it now:
$extended = $this->job->extended_status;
if ($extended['steps'] === 0) {
$extended['steps'] = $index * 5;
$this->job->extended_status = $extended;
$this->job->save();
}
Log::debug(sprintf('Number of entries: %d', $entries->count()));
$notImported = $entries->filter(
function (array $row, int $index) {
if ($this->rowAlreadyImported($row)) {
$message = sprintf('Row #%d has already been imported.', $index);
$this->job->addError($index, $message);
$this->job->addStepsDone(5); // all steps.
Log::info($message);
return null;
}
return $row;
}
);
Log::debug(sprintf('Number of entries left: %d', $notImported->count()));
// set (new) number of steps:
$status = $this->job->extended_status;
$status['steps'] = $notImported->count() * 5;
$this->job->extended_status = $status;
$this->job->save();
Log::debug(sprintf('Number of steps: %d', $notImported->count() * 5));
$notImported->each(
function (array $row, int $index) {
$journal = $this->importRow($index, $row);
$this->objects->push($journal);
$this->job->addStepsDone(1);
}
);
return true;
}
/**
* Set import job for this processor.
*
* @param ImportJob $job
*
* @return FileProcessorInterface
@@ -116,7 +128,6 @@ class CsvProcessor implements FileProcessorInterface
*/
private function annotateValue(int $index, string $value)
{
$value = trim($value);
$config = $this->job->configuration;
$role = $config['column-roles'][$index] ?? '_ignore';
$mapped = $config['column-mapping-config'][$index][$value] ?? null;
@@ -151,6 +162,64 @@ class CsvProcessor implements FileProcessorInterface
return $results;
}
/**
* Will return string representation of JSON error code.
*
* @param int $jsonError
*
* @return string
*/
private function getJsonError(int $jsonError): string
{
switch ($jsonError) {
default:
return 'Unknown JSON error';
case JSON_ERROR_NONE:
return 'No JSON error';
case JSON_ERROR_DEPTH:
return 'JSON_ERROR_DEPTH';
case JSON_ERROR_STATE_MISMATCH:
return 'JSON_ERROR_STATE_MISMATCH';
case JSON_ERROR_CTRL_CHAR:
return 'JSON_ERROR_CTRL_CHAR';
case JSON_ERROR_SYNTAX:
return 'JSON_ERROR_SYNTAX';
case JSON_ERROR_UTF8:
return 'JSON_ERROR_UTF8';
case JSON_ERROR_RECURSION:
return 'JSON_ERROR_RECURSION';
case JSON_ERROR_INF_OR_NAN:
return 'JSON_ERROR_INF_OR_NAN';
case JSON_ERROR_UNSUPPORTED_TYPE:
return 'JSON_ERROR_UNSUPPORTED_TYPE';
case JSON_ERROR_INVALID_PROPERTY_NAME:
return 'JSON_ERROR_INVALID_PROPERTY_NAME';
case JSON_ERROR_UTF16:
return 'JSON_ERROR_UTF16';
}
}
/**
* Hash an array and return the result.
*
* @param array $array
*
* @return string
* @throws FireflyException
*/
private function getRowHash(array $array): string
{
$json = json_encode($array);
$jsonError = json_last_error();
if ($json === false) {
throw new FireflyException(sprintf('Error while encoding JSON for CSV row: %s', $this->getJsonError($jsonError)));
}
$hash = hash('sha256', $json);
return $hash;
}
/**
* Take a row, build import journal by annotating each value and storing it in the import journal.
*
@@ -158,15 +227,22 @@ class CsvProcessor implements FileProcessorInterface
* @param array $row
*
* @return ImportJournal
* @throws FireflyException
*/
private function importRow(int $index, array $row): ImportJournal
{
Log::debug(sprintf('Now at row %d', $index));
$row = $this->specifics($row);
$row = $this->specifics($row);
$hash = $this->getRowHash($row);
$journal = new ImportJournal;
$journal->setUser($this->job->user);
$journal->setHash(hash('sha256', json_encode($row)));
$journal->setHash($hash);
/**
* @var int $rowIndex
* @var string $value
*/
foreach ($row as $rowIndex => $value) {
$value = trim($value);
if (strlen($value) > 0) {
@@ -175,7 +251,8 @@ class CsvProcessor implements FileProcessorInterface
$journal->setValue($annotated);
}
}
Log::debug('ImportJournal complete, returning.');
// set some extra info:
$journal->asset->setDefaultAccountId($this->job->configuration['import-account']);
return $journal;
}
@@ -189,13 +266,12 @@ class CsvProcessor implements FileProcessorInterface
*/
private function rowAlreadyImported(array $array): bool
{
$string = json_encode($array);
$hash = hash('sha256', json_encode($string));
$json = json_encode($hash);
$entry = TransactionJournalMeta::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id')
->where('data', $json)
->where('name', 'importHash')
->first();
$hash = $this->getRowHash($array);
$json = json_encode($hash);
$entry = TransactionJournalMeta::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id')
->where('data', $json)
->where('name', 'importHash')
->first();
if (!is_null($entry)) {
return true;
}
@@ -215,8 +291,8 @@ class CsvProcessor implements FileProcessorInterface
private function specifics(array $row): array
{
$config = $this->job->configuration;
//
foreach ($config['specifics'] as $name => $enabled) {
$names = array_keys($config['specifics']);
foreach ($names as $name) {
if (!in_array($name, $this->validSpecifics)) {
throw new FireflyException(sprintf('"%s" is not a valid class name', $name));

View File

@@ -20,6 +20,7 @@ use Illuminate\Support\Collection;
use Log;
/**
*
* Class ImportAccount
*
* @package FireflyIII\Import\Object
@@ -41,6 +42,14 @@ class ImportAccount
private $defaultAccountId = 0;
/** @var string */
private $expectedType = '';
/**
* This value is used to indicate the other account ID (the opposing transaction's account),
* if it is know. If so, this particular importaccount may never return an Account with this ID.
* If it would, this would result in a transaction from-to the same account.
*
* @var int
*/
private $forbiddenAccountId = 0;
/** @var AccountRepositoryInterface */
private $repository;
/** @var User */
@@ -125,6 +134,14 @@ class ImportAccount
$this->defaultAccountId = $defaultAccountId;
}
/**
* @param int $forbiddenAccountId
*/
public function setForbiddenAccountId(int $forbiddenAccountId)
{
$this->forbiddenAccountId = $forbiddenAccountId;
}
/**
* @param User $user
*/
@@ -148,7 +165,9 @@ class ImportAccount
if (count($this->accountId) === 3) {
Log::debug(sprintf('Finding account of type %d and ID %d', $accountType->id, $this->accountId['value']));
/** @var Account $account */
$account = $this->user->accounts()->where('account_type_id', $accountType->id)->where('id', $this->accountId['value'])->first();
$account = $this->user->accounts()->where('id', '!=', $this->forbiddenAccountId)->where('account_type_id', $accountType->id)->where(
'id', $this->accountId['value']
)->first();
if (!is_null($account)) {
Log::debug(sprintf('Found unmapped %s account by ID (#%d): %s', $this->expectedType, $account->id, $account->name));
@@ -164,7 +183,7 @@ class ImportAccount
Log::debug(sprintf('Finding account of type %d and IBAN %s', $accountType->id, $iban));
$filtered = $accounts->filter(
function (Account $account) use ($iban) {
if ($account->iban === $iban) {
if ($account->iban === $iban && $account->id !== $this->forbiddenAccountId) {
Log::debug(
sprintf('Found unmapped %s account by IBAN (#%d): %s (%s)', $this->expectedType, $account->id, $account->name, $account->iban)
);
@@ -187,7 +206,7 @@ class ImportAccount
Log::debug(sprintf('Finding account of type %d and name %s', $accountType->id, $name));
$filtered = $accounts->filter(
function (Account $account) use ($name) {
if ($account->name === $name) {
if ($account->name === $name && $account->id !== $this->forbiddenAccountId) {
Log::debug(sprintf('Found unmapped %s account by name (#%d): %s', $this->expectedType, $account->id, $account->name));
return $account;
@@ -316,7 +335,7 @@ class ImportAccount
}
$this->expectedType = $oldExpectedType;
// if search for an asset account, fall back to given "default account" (mandatory)
// 4: if search for an asset account, fall back to given "default account" (mandatory)
if ($this->expectedType === AccountType::ASSET) {
$this->account = $this->repository->find($this->defaultAccountId);
Log::debug(sprintf('Fall back to default account #%d "%s"', $this->account->id, $this->account->name));
@@ -324,6 +343,7 @@ class ImportAccount
return true;
}
// 5: then maybe, create one:
Log::debug(sprintf('Found no account of type %s so must create one ourselves.', $this->expectedType));
$data = [

View File

@@ -16,9 +16,8 @@ use Carbon\Carbon;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Import\Converter\Amount;
use FireflyIII\Import\Converter\ConverterInterface;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Import\MapperPreProcess\PreProcessorInterface;
use FireflyIII\User;
use Illuminate\Support\Collection;
use InvalidArgumentException;
use Log;
use Steam;
@@ -40,8 +39,6 @@ class ImportJournal
public $category;
/** @var string */
public $description = '';
/** @var Collection */
public $errors;
/** @var string */
public $hash;
/** @var array */
@@ -50,18 +47,18 @@ class ImportJournal
public $notes = '';
/** @var ImportAccount */
public $opposing;
/** @var array */
public $tags = [];
/** @var string */
private $amount = '0';
private $amount;
/** @var ImportCurrency */
private $currency;
public $currency;
/** @var string */
private $date = '';
/** @var string */
private $externalId = '';
/** @var array */
private $modifiers = [];
/** @var array */
private $tags = [];
/** @var User */
private $user;
@@ -70,7 +67,6 @@ class ImportJournal
*/
public function __construct()
{
$this->errors = new Collection;
$this->asset = new ImportAccount;
$this->opposing = new ImportAccount;
$this->bill = new ImportBill;
@@ -87,45 +83,29 @@ class ImportJournal
$this->modifiers[] = $modifier;
}
/**
* @return TransactionJournal
* @throws FireflyException
*/
public function createTransactionJournal(): TransactionJournal
{
exit('does not work yet');
}
/**
* @return string
*/
public function getAmount(): string
{
/** @var ConverterInterface $amountConverter */
$amountConverter = app(Amount::class);
$this->amount = $amountConverter->convert($this->amount);
// modify
foreach ($this->modifiers as $modifier) {
$class = sprintf('FireflyIII\Import\Converter\%s', config(sprintf('csv.import_roles.%s.converter', $modifier['role'])));
/** @var ConverterInterface $converter */
$converter = app($class);
if ($converter->convert($modifier['value']) === -1) {
$this->amount = Steam::negative($this->amount);
if (is_null($this->amount)) {
/** @var ConverterInterface $amountConverter */
$amountConverter = app(Amount::class);
$this->amount = $amountConverter->convert($this->amount);
// modify
foreach ($this->modifiers as $modifier) {
$class = sprintf('FireflyIII\Import\Converter\%s', config(sprintf('csv.import_roles.%s.converter', $modifier['role'])));
/** @var ConverterInterface $converter */
$converter = app($class);
if ($converter->convert($modifier['value']) === -1) {
$this->amount = Steam::negative($this->amount);
}
}
}
return $this->amount;
}
/**
* @return ImportCurrency
*/
public function getCurrency(): ImportCurrency
{
return $this->currency;
}
/**
* @param string $format
*
@@ -269,7 +249,7 @@ class ImportJournal
break;
case 'tags-comma':
case 'tags-space':
$this->tags[] = $array;
$this->setTags($array);
break;
case 'date-interest':
$this->metaDates['interest_date'] = $array['value'];
@@ -282,4 +262,18 @@ class ImportJournal
break;
}
}
/**
* @param array $array
*/
private function setTags(array $array): void
{
$preProcessorClass = config(sprintf('csv.import_roles.%s.pre-process-mapper', $array['role']));
/** @var PreProcessorInterface $preProcessor */
$preProcessor = app(sprintf('\FireflyIII\Import\MapperPreProcess\%s', $preProcessorClass));
$tags = $preProcessor->run($array['value']);
$this->tags = array_merge($this->tags, $tags);
return;
}
}

View File

@@ -11,28 +11,12 @@ declare(strict_types=1);
namespace FireflyIII\Import\Storage;
use Amount;
use Carbon\Carbon;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Import\Object\ImportAccount;
use FireflyIII\Import\Object\ImportJournal;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\Bill;
use FireflyIII\Models\Budget;
use FireflyIII\Models\Category;
use FireflyIII\Models\ImportJob;
use FireflyIII\Models\Rule;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionCurrency;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Currency\CurrencyRepositoryInterface;
use FireflyIII\Rules\Processor;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Collection;
use Log;
use Steam;
/**
* Is capable of storing individual ImportJournal objects.
@@ -42,16 +26,16 @@ use Steam;
*/
class ImportStorage
{
use ImportSupport;
/** @var Collection */
public $errors;
/** @var Collection */
public $journals;
/** @var CurrencyRepositoryInterface */
private $currencyRepository;
/** @var int */
protected $defaultCurrencyId = 1;
/** @var string */
private $dateFormat = 'Ymd';
/** @var TransactionCurrency */
private $defaultCurrency;
private $dateFormat = 'Ymd'; // yes, hard coded
/** @var ImportJob */
private $job;
/** @var Collection */
@@ -59,6 +43,9 @@ class ImportStorage
/** @var Collection */
private $rules;
/** @var array */
private $transfers = [];
/**
* ImportStorage constructor.
*/
@@ -82,11 +69,11 @@ class ImportStorage
*/
public function setJob(ImportJob $job)
{
$this->job = $job;
$repository = app(CurrencyRepositoryInterface::class);
$repository->setUser($job->user);
$this->currencyRepository = $repository;
$this->rules = $this->getUserRules();
$this->job = $job;
$currency = app('amount')->getDefaultCurrencyByUser($this->job->user);
$this->defaultCurrencyId = $currency->id;
$this->transfers = $this->getTransfers();
$this->rules = $this->getRules();
}
/**
@@ -98,316 +85,82 @@ class ImportStorage
}
/**
* Do storage of import objects.
* Do storage of import objects. Is the main function.
*
* @return bool
*/
public function store()
public function store(): bool
{
$this->defaultCurrency = Amount::getDefaultCurrencyByUser($this->job->user);
/**
* @var int $index
* @var ImportJournal $object
*/
foreach ($this->objects as $index => $object) {
try {
$this->storeImportJournal($index, $object);
} catch (FireflyException $e) {
$this->errors->push($e->getMessage());
Log::error(sprintf('Cannot import row #%d because: %s', $index, $e->getMessage()));
$this->objects->each(
function (ImportJournal $importJournal, int $index) {
try {
$this->storeImportJournal($index, $importJournal);
} catch (FireflyException $e) {
$this->errors->push($e->getMessage());
Log::error(sprintf('Cannot import row #%d because: %s', $index, $e->getMessage()));
}
}
}
);
Log::info('ImportStorage has finished.');
return true;
}
/**
* @param TransactionJournal $journal
*
* @return bool
*/
protected function applyRules(TransactionJournal $journal): bool
{
if ($this->rules->count() > 0) {
/** @var Rule $rule */
foreach ($this->rules as $rule) {
Log::debug(sprintf('Going to apply rule #%d to journal %d.', $rule->id, $journal->id));
$processor = Processor::make($rule);
$processor->handleTransactionJournal($journal);
if ($rule->stop_processing) {
return true;
}
}
}
return true;
}
/**
* @param int $journalId
* @param int $accountId
* @param int $currencyId
* @param string $amount
* @param int $index
* @param ImportJournal $importJournal
*
* @return bool
* @throws FireflyException
*/
private function createTransaction(int $journalId, int $accountId, int $currencyId, string $amount): bool
{
$transaction = new Transaction;
$transaction->account_id = $accountId;
$transaction->transaction_journal_id = $journalId;
$transaction->transaction_currency_id = $currencyId;
$transaction->amount = $amount;
$transaction->save();
if (is_null($transaction->id)) {
$errorText = join(', ', $transaction->getErrors()->all());
throw new FireflyException($errorText);
}
Log::debug(sprintf('Created transaction with ID #%d, account #%d, amount %s', $transaction->id, $accountId, $amount));
return true;
}
/**
* @param Collection $set
* @param ImportJournal $importJournal
*
* @return bool
*/
private function filterTransferSet(Collection $set, ImportJournal $importJournal): bool
{
$amount = Steam::positive($importJournal->getAmount());
$asset = $importJournal->asset->getAccount();
$opposing = $this->getOpposingAccount($importJournal->opposing, $amount);
$description = $importJournal->getDescription();
$filtered = $set->filter(
function (TransactionJournal $journal) use ($asset, $opposing, $description) {
$match = true;
$original = [app('steam')->tryDecrypt($journal->source_name), app('steam')->tryDecrypt($journal->destination_name)];
$compare = [$asset->name, $opposing->name];
sort($original);
sort($compare);
// description does not match? Then cannot be duplicate.
if ($journal->description !== $description) {
$match = false;
}
// not both accounts in journal? Then cannot be duplicate.
if ($original !== $compare) {
$match = false;
}
if ($match) {
return $journal;
}
return null;
}
);
if (count($filtered) > 0) {
return true;
}
return false;
}
/**
* @param ImportJournal $importJournal
*
* @param Account $account
*
* @return TransactionCurrency
*/
private function getCurrency(ImportJournal $importJournal, Account $account): TransactionCurrency
{
// start with currency pref of account, if any:
$currency = $this->currencyRepository->find(intval($account->getMeta('currency_id')));
if (!is_null($currency->id)) {
return $currency;
}
// use given currency
$currency = $importJournal->getCurrency()->getTransactionCurrency();
if (!is_null($currency->id)) {
return $currency;
}
// backup to default
$currency = $this->defaultCurrency;
return $currency;
}
/**
* @param ImportAccount $account
* @param $amount
*
* @return Account
*/
private function getOpposingAccount(ImportAccount $account, $amount): Account
{
if (bccomp($amount, '0') === -1) {
Log::debug(sprintf('%s is negative, create opposing expense account.', $amount));
$account->setExpectedType(AccountType::EXPENSE);
return $account->getAccount();
}
Log::debug(sprintf('%s is positive, create opposing revenue account.', $amount));
// amount is positive, it's a deposit, opposing is an revenue:
$account->setExpectedType(AccountType::REVENUE);
$databaseAccount = $account->getAccount();
return $databaseAccount;
}
/**
* @param string $amount
*
* @return TransactionType
*/
private function getTransactionType(string $amount): TransactionType
{
$transactionType = new TransactionType();
// amount is negative, it's a withdrawal, opposing is an expense:
if (bccomp($amount, '0') === -1) {
$transactionType = TransactionType::whereType(TransactionType::WITHDRAWAL)->first();
}
if (bccomp($amount, '0') === 1) {
$transactionType = TransactionType::whereType(TransactionType::DEPOSIT)->first();
}
return $transactionType;
}
/**
* @return Collection
*/
private function getUserRules(): Collection
{
$set = Rule::distinct()
->where('rules.user_id', $this->job->user->id)
->leftJoin('rule_groups', 'rule_groups.id', '=', 'rules.rule_group_id')
->leftJoin('rule_triggers', 'rules.id', '=', 'rule_triggers.rule_id')
->where('rule_groups.active', 1)
->where('rule_triggers.trigger_type', 'user_action')
->where('rule_triggers.trigger_value', 'store-journal')
->where('rules.active', 1)
->orderBy('rule_groups.order', 'ASC')
->orderBy('rules.order', 'ASC')
->get(['rules.*', 'rule_groups.order']);
Log::debug(sprintf('Found %d user rules.', $set->count()));
return $set;
}
/**
* @param TransactionJournal $journal
* @param Bill $bill
*/
private function storeBill(TransactionJournal $journal, Bill $bill)
{
if (!is_null($bill->id)) {
Log::debug(sprintf('Linked bill #%d to journal #%d', $bill->id, $journal->id));
$journal->bill()->associate($bill);
$journal->save();
}
}
/**
* @param TransactionJournal $journal
* @param Budget $budget
*/
private function storeBudget(TransactionJournal $journal, Budget $budget)
{
if (!is_null($budget->id)) {
Log::debug(sprintf('Linked budget #%d to journal #%d', $budget->id, $journal->id));
$journal->budgets()->save($budget);
}
}
/**
* @param TransactionJournal $journal
* @param Category $category
*/
private function storeCategory(TransactionJournal $journal, Category $category)
{
if (!is_null($category->id)) {
Log::debug(sprintf('Linked category #%d to journal #%d', $category->id, $journal->id));
$journal->categories()->save($category);
}
}
private function storeImportJournal(int $index, ImportJournal $importJournal): bool
protected function storeImportJournal(int $index, ImportJournal $importJournal): bool
{
Log::debug(sprintf('Going to store object #%d with description "%s"', $index, $importJournal->getDescription()));
$importJournal->asset->setDefaultAccountId($this->job->configuration['import-account']);
$asset = $importJournal->asset->getAccount();
$amount = $importJournal->getAmount();
$currency = $this->getCurrency($importJournal, $asset);
$date = $importJournal->getDate($this->dateFormat);
$transactionType = $this->getTransactionType($amount);
$opposing = $this->getOpposingAccount($importJournal->opposing, $amount);
// if opposing is an asset account, it's a transfer:
if ($opposing->accountType->type === AccountType::ASSET) {
Log::debug(sprintf('Opposing account #%d %s is an asset account, make transfer.', $opposing->id, $opposing->name));
$transactionType = TransactionType::whereType(TransactionType::TRANSFER)->first();
}
// verify that opposing account is of the correct type:
if ($opposing->accountType->type === AccountType::EXPENSE && $transactionType->type !== TransactionType::WITHDRAWAL) {
$message = sprintf('Row #%d is imported as a %s but opposing is an expense account. This cannot be!', $index, $transactionType->type);
Log::error($message);
throw new FireflyException($message);
}
$assetAccount = $importJournal->asset->getAccount();
$amount = $importJournal->getAmount();
$currencyId = $this->getCurrencyId($importJournal);
$foreignCurrencyId = $this->getForeignCurrencyId($importJournal, $currencyId);
$date = $importJournal->getDate($this->dateFormat)->format('Y-m-d');
$opposingAccount = $this->getOpposingAccount($importJournal->opposing, $assetAccount->id, $amount);
$transactionType = $this->getTransactionType($amount, $opposingAccount);
$description = $importJournal->getDescription();
/*** First step done! */
$this->job->addStepsDone(1);
// could be that transfer is double: verify this.
if ($this->verifyDoubleTransfer($transactionType, $importJournal)) {
// add three steps:
/**
* Check for double transfer.
*/
$parameters = [
'type' => $transactionType,
'description' => $description,
'amount' => $amount,
'date' => $date,
'asset' => $assetAccount->name,
'opposing' => $opposingAccount->name,
];
if ($this->isDoubleTransfer($parameters) || $this->hashAlreadyImported($importJournal->hash)) {
$this->job->addStepsDone(3);
// throw error
throw new FireflyException('Detected a possible duplicate, skip this one.');
}
unset($parameters);
// create a journal:
$journal = new TransactionJournal;
$journal->user_id = $this->job->user_id;
$journal->transaction_type_id = $transactionType->id;
$journal->transaction_currency_id = $currency->id;
$journal->description = $importJournal->getDescription();
$journal->date = $date->format('Y-m-d');
$journal->order = 0;
$journal->tag_count = 0;
$journal->completed = false;
// store journal and create transactions:
$parameters = [
'type' => $transactionType,
'currency' => $currencyId,
'foreign_currency' => $foreignCurrencyId,
'asset' => $assetAccount,
'opposing' => $opposingAccount,
'description' => $description,
'date' => $date,
'hash' => $importJournal->hash,
'amount' => $amount,
if (!$journal->save()) {
$errorText = join(', ', $journal->getErrors()->all());
// add three steps:
$this->job->addStepsDone(3);
// throw error
throw new FireflyException($errorText);
}
// save meta data:
$journal->setMeta('importHash', $importJournal->hash);
Log::debug(sprintf('Created journal with ID #%d', $journal->id));
// create transactions:
$this->createTransaction($journal->id, $asset->id, $currency->id, $amount);
$this->createTransaction($journal->id, $opposing->id, $currency->id, Steam::opposite($amount));
];
$journal = $this->storeJournal($parameters);
unset($parameters);
/*** Another step done! */
$this->job->addStepsDone(1);
@@ -417,94 +170,57 @@ class ImportStorage
$this->storeBudget($journal, $importJournal->budget->getBudget());
$this->storeBill($journal, $importJournal->bill->getBill());
$this->storeMeta($journal, $importJournal->metaDates);
// sepa thing as note:
if (strlen($importJournal->notes) > 0) {
$journal->setMeta('notes', $importJournal->notes);
}
$journal->setMeta('notes', $importJournal->notes);
$this->storeTags($importJournal->tags, $journal);
// set journal completed:
$journal->completed = true;
$journal->save();
/*** Another step done! */
$this->job->addStepsDone(1);
// run rules:
$this->applyRules($journal);
/*** Another step done! */
$this->job->addStepsDone(1);
$this->journals->push($journal);
Log::info(
sprintf(
'Imported new journal #%d with description "%s" and amount %s %s.', $journal->id, $journal->description, $journal->transactionCurrency->code,
$amount
)
);
Log::info(sprintf('Imported new journal #%d: "%s", amount %s %s.', $journal->id, $journal->description, $journal->transactionCurrency->code, $amount));
return true;
}
/**
* @param TransactionJournal $journal
* @param array $dates
*/
private function storeMeta(TransactionJournal $journal, array $dates)
{
// all other date fields as meta thing:
foreach ($dates as $name => $value) {
try {
$date = new Carbon($value);
$journal->setMeta($name, $date);
} catch (\Exception $e) {
// don't care, ignore:
Log::warning(sprintf('Could not parse "%s" into a valid Date object for field %s', $value, $name));
}
}
}
/**
* This method checks if the given transaction is a transfer and if so, if it might be a duplicate of an already imported transfer.
* This is important for import files that cover multiple accounts (and include both A<>B and B<>A transactions).
*
* @param TransactionType $transactionType
* @param ImportJournal $importJournal
* @param array $parameters
*
* @return bool
*/
private function verifyDoubleTransfer(TransactionType $transactionType, ImportJournal $importJournal): bool
private function isDoubleTransfer(array $parameters): bool
{
if ($transactionType->type === TransactionType::TRANSFER) {
$amount = Steam::positive($importJournal->getAmount());
$date = $importJournal->getDate($this->dateFormat);
$set = TransactionJournal::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
->leftJoin(
'transactions AS source', function (JoinClause $join) {
$join->on('transaction_journals.id', '=', 'source.transaction_journal_id')->where('source.amount', '<', 0);
}
)
->leftJoin(
'transactions AS destination', function (JoinClause $join) {
$join->on('transaction_journals.id', '=', 'destination.transaction_journal_id')->where(
'destination.amount', '>', 0
);
}
)
->leftJoin('accounts as source_accounts', 'source.account_id', '=', 'source_accounts.id')
->leftJoin('accounts as destination_accounts', 'destination.account_id', '=', 'destination_accounts.id')
->where('transaction_journals.user_id', $this->job->user_id)
->where('transaction_types.type', TransactionType::TRANSFER)
->where('transaction_journals.date', $date->format('Y-m-d'))
->where('destination.amount', $amount)
->get(
['transaction_journals.id', 'transaction_journals.encrypted', 'transaction_journals.description',
'source_accounts.name as source_name', 'destination_accounts.name as destination_name']
);
return $this->filterTransferSet($set, $importJournal);
if ($parameters['type'] !== TransactionType::TRANSFER) {
return false;
}
$amount = app('steam')->positive($parameters['amount']);
$names = [$parameters['asset'], $parameters['opposing']];
sort($names);
return false;
foreach ($this->transfers as $transfer) {
if ($parameters['description'] !== $transfer['description']) {
return false;
}
if ($names !== $transfer['names']) {
return false;
}
if (bccomp($amount, $transfer['amount']) !== 0) {
return false;
}
if ($parameters['date'] !== $transfer['date']) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,444 @@
<?php
/**
* ImportSupport.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\Import\Storage;
use Carbon\Carbon;
use Exception;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Import\Object\ImportAccount;
use FireflyIII\Import\Object\ImportJournal;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\Bill;
use FireflyIII\Models\Budget;
use FireflyIII\Models\Category;
use FireflyIII\Models\Rule;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionJournalMeta;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Tag\TagRepositoryInterface;
use FireflyIII\Rules\Processor;
use Illuminate\Database\Query\JoinClause;
use Illuminate\Support\Collection;
use Log;
trait ImportSupport
{
/** @var int */
protected $defaultCurrencyId = 1;
/**
* @param TransactionJournal $journal
*
* @return bool
*/
protected function applyRules(TransactionJournal $journal): bool
{
if ($this->rules->count() > 0) {
$this->rules->each(
function (Rule $rule) use ($journal) {
Log::debug(sprintf('Going to apply rule #%d to journal %d.', $rule->id, $journal->id));
$processor = Processor::make($rule);
$processor->handleTransactionJournal($journal);
if ($rule->stop_processing) {
return false;
}
return true;
}
);
}
return true;
}
/**
* @param array $parameters
*
* @return bool
* @throws FireflyException
*/
private function createTransaction(array $parameters): bool
{
$transaction = new Transaction;
$transaction->account_id = $parameters['account'];
$transaction->transaction_journal_id = $parameters['id'];
$transaction->transaction_currency_id = $parameters['currency'];
$transaction->amount = $parameters['amount'];
$transaction->foreign_currency_id = $parameters['foreign_currency'];
$transaction->foreign_amount = $parameters['foreign_amount'];
$transaction->save();
if (is_null($transaction->id)) {
$errorText = join(', ', $transaction->getErrors()->all());
throw new FireflyException($errorText);
}
Log::debug(sprintf('Created transaction with ID #%d, account #%d, amount %s', $transaction->id, $parameters['account'], $parameters['amount']));
return true;
}
/**
* This method finds out what the import journal's currency should be. The account itself
* is favoured (and usually it stops there). If no preference is found, the journal has a say
* and thirdly the default currency is used.
*
* @param ImportJournal $importJournal
*
* @return int
*/
private function getCurrencyId(ImportJournal $importJournal): int
{
// start with currency pref of account, if any:
$account = $importJournal->asset->getAccount();
$currencyId = intval($account->getMeta('currency_id'));
if ($currencyId > 0) {
return $currencyId;
}
// use given currency
$currency = $importJournal->currency->getTransactionCurrency();
if (!is_null($currency->id)) {
return $currency->id;
}
// backup to default
$currency = $this->defaultCurrencyId;
return $currency;
}
/**
* The foreign currency is only returned when the journal has a different value from the
* currency id (see other method).
*
* @param ImportJournal $importJournal
* @param int $currencyId
*
* @see ImportSupport::getCurrencyId
*
* @return int|null
*/
private function getForeignCurrencyId(ImportJournal $importJournal, int $currencyId): ?int
{
// use given currency by import journal.
$currency = $importJournal->currency->getTransactionCurrency();
if (!is_null($currency->id) && $currency->id !== $currencyId) {
return $currency->id;
}
// return null, because no different:
return null;
}
/**
* The search for the opposing account is complex. Firstly, we forbid the ImportAccount to resolve into the asset
* account to prevent a situation where the transaction flows from A to A. Given the amount, we "expect" the opposing
* account to be an expense or a revenue account. However, the mapping given by the user may return something else
* entirely (usually an asset account). So whatever the expectation, the result may be anything.
*
* When the result does not match the expected type (a negative amount cannot be linked to a revenue account) the next step
* will return an error.
*
* @param ImportAccount $account
* @param int $forbiddenAccount
* @param string $amount
*
* @see ImportSupport::getTransactionType
*
* @return Account
*/
private function getOpposingAccount(ImportAccount $account, int $forbiddenAccount, string $amount): Account
{
$account->setForbiddenAccountId($forbiddenAccount);
if (bccomp($amount, '0') === -1) {
Log::debug(sprintf('%s is negative, create opposing expense account.', $amount));
$account->setExpectedType(AccountType::EXPENSE);
return $account->getAccount();
}
Log::debug(sprintf('%s is positive, create opposing revenue account.', $amount));
// amount is positive, it's a deposit, opposing is an revenue:
$account->setExpectedType(AccountType::REVENUE);
$databaseAccount = $account->getAccount();
return $databaseAccount;
}
/**
* @return Collection
*/
private function getRules(): Collection
{
$set = Rule::distinct()
->where('rules.user_id', $this->job->user->id)
->leftJoin('rule_groups', 'rule_groups.id', '=', 'rules.rule_group_id')
->leftJoin('rule_triggers', 'rules.id', '=', 'rule_triggers.rule_id')
->where('rule_groups.active', 1)
->where('rule_triggers.trigger_type', 'user_action')
->where('rule_triggers.trigger_value', 'store-journal')
->where('rules.active', 1)
->orderBy('rule_groups.order', 'ASC')
->orderBy('rules.order', 'ASC')
->get(['rules.*', 'rule_groups.order']);
Log::debug(sprintf('Found %d user rules.', $set->count()));
return $set;
}
/**
* Given the amount and the opposing account its easy to define which kind of transaction type should be associated with the new
* import. This may however fail when there is an unexpected mismatch between the transaction type and the opposing account.
*
* @param string $amount
* @param Account $account
*
* @return string
* @throws FireflyException
* @see ImportSupport::getOpposingAccount()
*/
private function getTransactionType(string $amount, Account $account): string
{
$transactionType = '';
// amount is negative, it's a withdrawal, opposing is an expense:
if (bccomp($amount, '0') === -1) {
$transactionType = TransactionType::WITHDRAWAL;
}
if (bccomp($amount, '0') === 1) {
$transactionType = TransactionType::DEPOSIT;
}
// if opposing is an asset account, it's a transfer:
if ($account->accountType->type === AccountType::ASSET) {
Log::debug(sprintf('Opposing account #%d %s is an asset account, make transfer.', $account->id, $account->name));
$transactionType = TransactionType::TRANSFER;
}
// verify that opposing account is of the correct type:
if ($account->accountType->type === AccountType::EXPENSE && $transactionType !== TransactionType::WITHDRAWAL) {
$message = 'This row is imported as a withdrawal but opposing is an expense account. This cannot be!';
Log::error($message);
throw new FireflyException($message);
}
return $transactionType;
}
/**
* This method returns a collection of the current transfers in the system and some meta data for
* this set. This can later be used to see if the journal that firefly is trying to import
* is not already present.
*
* @return array
*/
private function getTransfers(): array
{
$set = TransactionJournal::leftJoin('transaction_types', 'transaction_types.id', '=', 'transaction_journals.transaction_type_id')
->leftJoin(
'transactions AS source', function (JoinClause $join) {
$join->on('transaction_journals.id', '=', 'source.transaction_journal_id')->where('source.amount', '<', 0);
}
)
->leftJoin(
'transactions AS destination', function (JoinClause $join) {
$join->on('transaction_journals.id', '=', 'destination.transaction_journal_id')->where(
'destination.amount', '>', 0
);
}
)
->leftJoin('accounts as source_accounts', 'source.account_id', '=', 'source_accounts.id')
->leftJoin('accounts as destination_accounts', 'destination.account_id', '=', 'destination_accounts.id')
->where('transaction_journals.user_id', $this->job->user_id)
->where('transaction_types.type', TransactionType::TRANSFER)
->get(
['transaction_journals.id', 'transaction_journals.encrypted', 'transaction_journals.description',
'source_accounts.name as source_name', 'destination_accounts.name as destination_name', 'destination.amount'
, 'transaction_journals.date']
);
$array = [];
/** @var TransactionJournal $entry */
foreach ($set as $entry) {
$original = [app('steam')->tryDecrypt($entry->source_name), app('steam')->tryDecrypt($entry->destination_name)];
sort($original);
$array[] = [
'names' => $original,
'amount' => $entry->amount,
'date' => $entry->date->format('Y-m-d'),
'description' => $entry->description,
];
}
return $array;
}
/**
* Checks if the import journal has not been imported before.
*
* @param string $hash
*
* @return bool
*/
private function hashAlreadyImported(string $hash): bool
{
$json = json_encode($hash);
$entry = TransactionJournalMeta::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'journal_meta.transaction_journal_id')
->where('data', $json)
->where('name', 'importHash')
->first();
if (!is_null($entry)) {
return true;
}
return false;
}
/**
* @param TransactionJournal $journal
* @param Bill $bill
*/
private function storeBill(TransactionJournal $journal, Bill $bill)
{
if (!is_null($bill->id)) {
Log::debug(sprintf('Linked bill #%d to journal #%d', $bill->id, $journal->id));
$journal->bill()->associate($bill);
$journal->save();
}
}
/**
* @param TransactionJournal $journal
* @param Budget $budget
*/
private function storeBudget(TransactionJournal $journal, Budget $budget)
{
if (!is_null($budget->id)) {
Log::debug(sprintf('Linked budget #%d to journal #%d', $budget->id, $journal->id));
$journal->budgets()->save($budget);
}
}
/**
* @param TransactionJournal $journal
* @param Category $category
*/
private function storeCategory(TransactionJournal $journal, Category $category)
{
if (!is_null($category->id)) {
Log::debug(sprintf('Linked category #%d to journal #%d', $category->id, $journal->id));
$journal->categories()->save($category);
}
}
private function storeJournal(array $parameters): TransactionJournal
{
// find transaction type:
$transactionType = TransactionType::whereType($parameters['type'])->first();
// create a journal:
$journal = new TransactionJournal;
$journal->user_id = $this->job->user_id;
$journal->transaction_type_id = $transactionType->id;
$journal->transaction_currency_id = $parameters['currency'];
$journal->description = $parameters['description'];
$journal->date = $parameters['date'];
$journal->order = 0;
$journal->tag_count = 0;
$journal->completed = false;
if (!$journal->save()) {
$errorText = join(', ', $journal->getErrors()->all());
// add three steps:
$this->job->addStepsDone(3);
// throw error
throw new FireflyException($errorText);
}
// save meta data:
$journal->setMeta('importHash', $parameters['hash']);
Log::debug(sprintf('Created journal with ID #%d', $journal->id));
// create transactions:
$one = [
'id' => $journal->id,
'account' => $parameters['asset']->id,
'currency' => $parameters['currency'],
'amount' => $parameters['amount'],
'foreign_currency' => $parameters['foreign_currency'],
'foreign_amount' => is_null($parameters['foreign_currency']) ? null : $parameters['amount'],
];
$opposite = app('steam')->opposite($parameters['amount']);
$two = [
'id' => $journal->id,
'account' => $parameters['opposing']->id,
'currency' => $parameters['currency'],
'amount' => $opposite,
'foreign_currency' => $parameters['foreign_currency'],
'foreign_amount' => is_null($parameters['foreign_currency']) ? null : $opposite,
];
$this->createTransaction($one);
$this->createTransaction($two);
return $journal;
}
/**
* @param TransactionJournal $journal
* @param array $dates
*/
private function storeMeta(TransactionJournal $journal, array $dates)
{
// all other date fields as meta thing:
foreach ($dates as $name => $value) {
try {
$date = new Carbon($value);
$journal->setMeta($name, $date);
} catch (Exception $e) {
// don't care, ignore:
Log::warning(sprintf('Could not parse "%s" into a valid Date object for field %s', $value, $name));
}
}
}
/**
* @param array $tags
* @param TransactionJournal $journal
*/
private function storeTags(array $tags, TransactionJournal $journal): void
{
$repository = app(TagRepositoryInterface::class);
$repository->setUser($journal->user);
foreach ($tags as $tag) {
$dbTag = $repository->findByTag($tag);
if (is_null($dbTag->id)) {
$dbTag = $repository->store(
['tag' => $tag, 'date' => null, 'description' => null, 'latitude' => null, 'longitude' => null,
'zoomLevel' => null, 'tagMode' => 'nothing']
);
}
$journal->tags()->save($dbTag);
Log::debug(sprintf('Linked tag %d ("%s") to journal #%d', $dbTag->id, $dbTag->tag, $journal->id));
}
return;
}
}

View File

@@ -1,4 +1,16 @@
<?php
declare(strict_types=1);
/**
* RegisteredUser.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.
*/
namespace FireflyIII\Mail;

View File

@@ -1,4 +1,16 @@
<?php
declare(strict_types=1);
/**
* RequestedNewPassword.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.
*/
namespace FireflyIII\Mail;

View File

@@ -35,6 +35,8 @@ use FireflyIII\Helpers\Report\ReportHelper;
use FireflyIII\Helpers\Report\ReportHelperInterface;
use FireflyIII\Repositories\User\UserRepository;
use FireflyIII\Repositories\User\UserRepositoryInterface;
use FireflyIII\Services\Password\PwndVerifier;
use FireflyIII\Services\Password\Verifier;
use FireflyIII\Support\Amount;
use FireflyIII\Support\ExpandedForm;
use FireflyIII\Support\FireflyConfig;
@@ -147,6 +149,9 @@ class FireflyServiceProvider extends ServiceProvider
$this->app->bind(FiscalHelperInterface::class, FiscalHelper::class);
$this->app->bind(BalanceReportHelperInterface::class, BalanceReportHelper::class);
$this->app->bind(BudgetReportHelperInterface::class, BudgetReportHelper::class);
// password verifier thing
$this->app->bind(Verifier::class, PwndVerifier::class);
}
}

View File

@@ -179,7 +179,7 @@ class AccountRepository implements AccountRepositoryInterface
{
// update the account:
$account->name = $data['name'];
$account->active = $data['active'] === '1' ? true : false;
$account->active = $data['active'];
$account->virtual_balance = $data['virtualBalance'];
$account->iban = $data['iban'];
$account->save();
@@ -500,7 +500,7 @@ class AccountRepository implements AccountRepositoryInterface
*
* @return null|string
*/
private function filterIban(string $iban = null)
private function filterIban(?string $iban)
{
if (is_null($iban)) {
return null;

View File

@@ -206,7 +206,8 @@ class AccountTasker implements AccountTaskerInterface
$expenses[$opposingId]['count']++;
}
// do averages:
foreach ($expenses as $key => $entry) {
$keys = array_keys($expenses);
foreach ($keys as $key) {
if ($expenses[$key]['count'] > 1) {
$expenses[$key]['average'] = bcdiv($expenses[$key]['sum'], strval($expenses[$key]['count']));
}

View File

@@ -62,6 +62,36 @@ class AttachmentRepository implements AttachmentRepositoryInterface
return $disk->exists($attachment->fileName());
}
/**
* @param int $id
*
* @return Attachment
*/
public function find(int $id): Attachment
{
$attachment = $this->user->attachments()->find($id);
if (is_null($attachment)) {
return new Attachment;
}
return $attachment;
}
/**
* @param int $id
*
* @return Attachment
*/
public function findWithoutUser(int $id): Attachment
{
$attachment = Attachment::find($id);
if (is_null($attachment)) {
return new Attachment;
}
return $attachment;
}
/**
* @return Collection
*/

View File

@@ -40,6 +40,20 @@ interface AttachmentRepositoryInterface
*/
public function exists(Attachment $attachment): bool;
/**
* @param int $id
*
* @return Attachment
*/
public function find(int $id): Attachment;
/**
* @param int $id
*
* @return Attachment
*/
public function findWithoutUser(int $id): Attachment;
/**
* @return Collection
*/

View File

@@ -191,6 +191,9 @@ class JournalTasker implements JournalTaskerInterface
$journalId = intval($transaction->transaction_journal_id);
$identifier = intval($transaction->identifier);
// also add the virtual balance to the balance:
$virtualBalance = strval($transaction->account->virtual_balance);
// go!
$sum = Transaction::leftJoin('transaction_journals', 'transaction_journals.id', '=', 'transactions.transaction_journal_id')
->where('account_id', $transaction->account_id)
@@ -224,6 +227,6 @@ class JournalTasker implements JournalTaskerInterface
}
)->sum('transactions.amount');
return strval($sum);
return bcadd(strval($sum), $virtualBalance);
}
}

View File

@@ -44,7 +44,7 @@ class RuleGroupRepository implements RuleGroupRepositoryInterface
*
* @return bool
*/
public function destroy(RuleGroup $ruleGroup, RuleGroup $moveTo = null): bool
public function destroy(RuleGroup $ruleGroup, ?RuleGroup $moveTo): bool
{
/** @var Rule $rule */
foreach ($ruleGroup->rules as $rule) {

View File

@@ -39,7 +39,7 @@ interface RuleGroupRepositoryInterface
*
* @return bool
*/
public function destroy(RuleGroup $ruleGroup, RuleGroup $moveTo = null): bool;
public function destroy(RuleGroup $ruleGroup, ?RuleGroup $moveTo): bool;
/**
* @param int $ruleGroupId

View File

@@ -51,21 +51,11 @@ class TagRepository implements TagRepositoryInterface
return false;
}
Log::debug(sprintf('Tag #%d connected', $tag->id));
$journal->tags()->save($tag);
$journal->save();
switch ($tag->tagMode) {
case 'nothing':
Log::debug(sprintf('Tag #%d connected', $tag->id));
$journal->tags()->save($tag);
$journal->save();
return true;
case 'balancingAct':
return $this->connectBalancingAct($journal, $tag);
case 'advancePayment':
return $this->connectAdvancePayment($journal, $tag);
}
return false;
return true;
}
/**
@@ -237,7 +227,7 @@ class TagRepository implements TagRepositoryInterface
$tag->latitude = $data['latitude'];
$tag->longitude = $data['longitude'];
$tag->zoomLevel = $data['zoomLevel'];
$tag->tagMode = $data['tagMode'];
$tag->tagMode = 'nothing';
$tag->user()->associate($this->user);
$tag->save();
@@ -253,7 +243,7 @@ class TagRepository implements TagRepositoryInterface
*
* @return string
*/
public function sumOfTag(Tag $tag, Carbon $start = null, Carbon $end = null): string
public function sumOfTag(Tag $tag, ?Carbon $start, ?Carbon $end): string
{
/** @var JournalCollectorInterface $collector */
$collector = app(JournalCollectorInterface::class);
@@ -268,73 +258,6 @@ class TagRepository implements TagRepositoryInterface
return strval($sum);
}
/**
* Can a tag become an advance payment?
*
* @param Tag $tag
*
* @return bool
*/
public function tagAllowAdvance(Tag $tag): bool
{
/*
* If this tag is a balancing act, and it contains transfers, it cannot be
* changed to an advancePayment.
*/
if ($tag->tagMode === 'balancingAct' || $tag->tagMode === 'nothing') {
foreach ($tag->transactionjournals as $journal) {
if ($journal->isTransfer()) {
return false;
}
}
}
/*
* If this tag contains more than one expenses, it cannot become an advance payment.
*/
$count = 0;
foreach ($tag->transactionjournals as $journal) {
if ($journal->isWithdrawal()) {
$count++;
}
}
if ($count > 1) {
return false;
}
return true;
}
/**
* Can a tag become a balancing act?
*
* @param Tag $tag
*
* @return bool
*/
public function tagAllowBalancing(Tag $tag): bool
{
/*
* If has more than two transactions already, cannot become a balancing act:
*/
if ($tag->transactionjournals->count() > 2) {
return false;
}
/*
* If any transaction is a deposit, cannot become a balancing act.
*/
foreach ($tag->transactionjournals as $journal) {
if ($journal->isDeposit()) {
return false;
}
}
return true;
}
/**
* @param Tag $tag
* @param array $data
@@ -349,183 +272,9 @@ class TagRepository implements TagRepositoryInterface
$tag->latitude = $data['latitude'];
$tag->longitude = $data['longitude'];
$tag->zoomLevel = $data['zoomLevel'];
$tag->tagMode = $data['tagMode'];
$tag->save();
return $tag;
}
/**
* @param TransactionJournal $journal
* @param Tag $tag
*
* @return bool
*/
protected function connectAdvancePayment(TransactionJournal $journal, Tag $tag): bool
{
$type = $journal->transactionType->type;
$withdrawals = $tag->transactionJournals()
->leftJoin('transaction_types', 'transaction_types.id', 'transaction_journals.transaction_type_id')
->where('transaction_types.type', TransactionType::WITHDRAWAL)->count();
$deposits = $tag->transactionJournals()
->leftJoin('transaction_types', 'transaction_types.id', 'transaction_journals.transaction_type_id')
->where('transaction_types.type', TransactionType::DEPOSIT)->count();
if ($type === TransactionType::TRANSFER) { // advance payments cannot accept transfers:
Log::error(sprintf('Journal #%d is a transfer and cannot connect to tag #%d', $journal->id, $tag->id));
return false;
}
// the first transaction to be attached to this tag is attached just like that:
if ($withdrawals < 1 && $deposits < 1) {
Log::debug(sprintf('Tag #%d has 0 withdrawals and 0 deposits so its fine.', $tag->id));
$journal->tags()->save($tag);
$journal->save();
return true;
}
// if withdrawal and already has a withdrawal, return false:
if ($type === TransactionType::WITHDRAWAL && $withdrawals > 0) {
Log::error(sprintf('Journal #%d is a withdrawal but tag already has %d withdrawal(s).', $journal->id, $withdrawals));
return false;
}
// if already has transaction journals, must match ALL asset account id's:
if ($deposits > 0 || $withdrawals === 1) {
Log::debug('Need to match all asset accounts.');
return $this->matchAll($journal, $tag);
}
// this statement is unreachable.
return false;
}
/**
* @param TransactionJournal $journal
* @param Tag $tag
*
* @return bool
*/
protected function connectBalancingAct(TransactionJournal $journal, Tag $tag): bool
{
$type = $journal->transactionType->type;
$withdrawals = $tag->transactionJournals()
->leftJoin('transaction_types', 'transaction_types.id', 'transaction_journals.transaction_type_id')
->where('transaction_types.type', TransactionType::WITHDRAWAL)->count();
$transfers = $tag->transactionJournals()
->leftJoin('transaction_types', 'transaction_types.id', 'transaction_journals.transaction_type_id')
->where('transaction_types.type', TransactionType::TRANSFER)->count();
Log::debug(sprintf('Journal #%d is a %s', $journal->id, $type));
// only if this is the only withdrawal.
if ($type === TransactionType::WITHDRAWAL && $withdrawals < 1) {
Log::debug('Will connect this journal because it is the only withdrawal in this tag.');
$journal->tags()->save($tag);
$journal->save();
return true;
}
// and only if this is the only transfer
if ($type === TransactionType::TRANSFER && $transfers < 1) {
Log::debug('Will connect this journal because it is the only transfer in this tag.');
$journal->tags()->save($tag);
$journal->save();
return true;
}
Log::error(
sprintf(
'Tag #%d has %d withdrawals and %d transfers and cannot contain %s #%d',
$tag->id, $withdrawals, $transfers, $type, $journal->id
)
);
// ignore expense
return false;
}
/**
* The incoming journal ($journal)'s accounts (source accounts for a withdrawal, destination accounts for a deposit)
* must match the already existing transaction's accounts exactly.
*
* @param TransactionJournal $journal
* @param Tag $tag
*
*
* @return bool
*/
private function matchAll(TransactionJournal $journal, Tag $tag): bool
{
$journalSources = join(',', array_unique($journal->sourceAccountList()->pluck('id')->toArray()));
$journalDestinations = join(',', array_unique($journal->destinationAccountList()->pluck('id')->toArray()));
$match = true;
$journals = $tag->transactionJournals()->get(['transaction_journals.*']);
Log::debug(sprintf('Tag #%d has %d journals to verify:', $tag->id, $journals->count()));
/** @var TransactionJournal $existing */
foreach ($journals as $existing) {
Log::debug(sprintf('Now existingcomparing new journal #%d to existing journal #%d', $journal->id, $existing->id));
// $checkAccount is the source_account for a withdrawal
// $checkAccount is the destination_account for a deposit
$existingSources = join(',', array_unique($existing->sourceAccountList()->pluck('id')->toArray()));
$existingDestinations = join(',', array_unique($existing->destinationAccountList()->pluck('id')->toArray()));
if ($existing->isWithdrawal() && $existingSources !== $journalDestinations) {
/*
* There can only be one withdrawal. And the source account(s) of the withdrawal
* must be the same as the destination of the deposit. Because any transaction that arrives
* here ($journal) must be a deposit.
*/
Log::debug(sprintf('Existing journal #%d is a withdrawal.', $existing->id));
Log::debug(sprintf('New journal #%d must have these destination accounts: %s', $journal->id, $existingSources));
Log::debug(sprintf('New journal #%d actually these destination accounts: %s', $journal->id, $journalDestinations));
Log::debug('So match is FALSE');
$match = false;
}
if ($existing->isDeposit() && $journal->isDeposit() && $existingDestinations !== $journalDestinations) {
/*
* There can be multiple deposits.
* They must have the destination the same as the other deposits.
*/
Log::debug(sprintf('Existing journal #%d is a deposit.', $existing->id));
Log::debug(sprintf('Journal #%d must have these destination accounts: %s', $journal->id, $existingDestinations));
Log::debug(sprintf('Journal #%d actually these destination accounts: %s', $journal->id, $journalDestinations));
Log::debug('So match is FALSE');
$match = false;
}
if ($existing->isDeposit() && $journal->isWithdrawal() && $existingDestinations !== $journalSources) {
/*
* There can be one new withdrawal only. It must have the same source as the existing has destination.
*/
Log::debug(sprintf('Existing journal #%d is a deposit.', $existing->id));
Log::debug(sprintf('Journal #%d must have these source accounts: %s', $journal->id, $existingDestinations));
Log::debug(sprintf('Journal #%d actually these source accounts: %s', $journal->id, $journalSources));
Log::debug('So match is FALSE');
$match = false;
}
}
if ($match) {
Log::debug(sprintf('Match is true, connect journal #%d with tag #%d.', $journal->id, $tag->id));
$journal->tags()->save($tag);
$journal->save();
return true;
}
return false;
}
}

View File

@@ -133,21 +133,7 @@ interface TagRepositoryInterface
*
* @return string
*/
public function sumOfTag(Tag $tag, Carbon $start = null, Carbon $end = null): string;
/**
* @param Tag $tag
*
* @return bool
*/
public function tagAllowAdvance(Tag $tag): bool;
/**
* @param Tag $tag
*
* @return bool
*/
public function tagAllowBalancing(Tag $tag): bool;
public function sumOfTag(Tag $tag, ?Carbon $start, ?Carbon $end): string;
/**
* Update a tag.

View File

@@ -0,0 +1,51 @@
<?php
/**
* PwndVerifier.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\Password;
use Log;
use Requests;
use Requests_Exception;
/**
* Class PwndVerifier
*
* @package FireflyIII\Services\Password
*/
class PwndVerifier implements Verifier
{
/**
* Verify the given password against (some) service.
*
* @param string $password
*
* @return bool
*/
public function validPassword(string $password): bool
{
$hash = sha1($password);
$uri = sprintf('https://haveibeenpwned.com/api/v2/pwnedpassword/%s', $hash);
$opt = ['useragent' => 'Firefly III v' . config('firefly.version'), 'timeout' => 2];
try {
$result = Requests::get($uri, ['originalPasswordIsAHash' => 'true'], $opt);
} catch (Requests_Exception $e) {
return true;
}
Log::debug(sprintf('Status code returned is %d', $result->status_code));
if ($result->status_code === 404) {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,30 @@
<?php
/**
* Verifier.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\Password;
/**
* Interface Verifier
*
* @package FireflyIII\Services\Password
*/
interface Verifier
{
/**
* Verify the given password against (some) service.
*
* @param string $password
*
* @return bool
*/
public function validPassword(string $password): bool;
}

View File

@@ -15,8 +15,6 @@ namespace FireflyIII\Support;
use Cache;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Collection;
use Preferences as Prefs;
@@ -97,25 +95,6 @@ class CacheProperties
{
$this->md5 = '';
foreach ($this->properties as $property) {
if ($property instanceof Collection || $property instanceof EloquentCollection) {
$this->md5 .= json_encode($property->toArray());
continue;
}
if ($property instanceof Carbon) {
$this->md5 .= $property->toRfc3339String();
continue;
}
if (is_object($property)) {
$this->md5 .= $property->__toString();
}
if (is_bool($property) && $property === false) {
$this->md5 .= 'false';
}
if (is_bool($property) && $property === true) {
$this->md5 .= 'true';
}
$this->md5 .= json_encode($property);
}
$this->md5 = md5($this->md5);

View File

@@ -40,8 +40,6 @@ class ExpandedForm
*/
public function amount(string $name, $value = null, array $options = []): string
{
$options['min'] = '0.01';
return $this->currencyField($name, 'amount', $value, $options);
}
@@ -54,8 +52,6 @@ class ExpandedForm
*/
public function amountSmall(string $name, $value = null, array $options = []): string
{
$options['min'] = '0.01';
return $this->currencyField($name, 'amount-small', $value, $options);
}
@@ -279,7 +275,6 @@ class ExpandedForm
$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']);

View File

@@ -27,6 +27,13 @@ interface ConfigurationInterface
*/
public function getData(): array;
/**
* Return possible warning to user.
*
* @return string
*/
public function getWarningMessage(): string;
/**
* @param ImportJob $job
*

View File

@@ -61,6 +61,16 @@ class Initial implements ConfigurationInterface
return $data;
}
/**
* Return possible warning to user.
*
* @return string
*/
public function getWarningMessage(): string
{
return '';
}
/**
* @param ImportJob $job
*
@@ -124,7 +134,8 @@ class Initial implements ConfigurationInterface
{
// loop specifics.
if (isset($data['specifics']) && is_array($data['specifics'])) {
foreach ($data['specifics'] as $name => $enabled) {
$names = array_keys($data['specifics']);
foreach ($names as $name) {
// verify their content.
$className = sprintf('FireflyIII\Import\Specifics\%s', $name);
if (class_exists($className)) {

View File

@@ -90,10 +90,12 @@ class Map implements ConfigurationInterface
}
}
}
foreach ($this->data as $index => $entry) {
$setIndexes = array_keys($this->data);
foreach ($setIndexes as $index) {
$this->data[$index]['values'] = array_unique($this->data[$index]['values']);
asort($this->data[$index]['values']);
}
unset($setIndexes);
// save number of rows, thus number of steps, in job:
$steps = $rowIndex * 5;
@@ -106,6 +108,16 @@ class Map implements ConfigurationInterface
}
/**
* Return possible warning to user.
*
* @return string
*/
public function getWarningMessage(): string
{
return '';
}
/**
* @param ImportJob $job
*
@@ -223,8 +235,8 @@ class Map implements ConfigurationInterface
{
// run specifics here:
// and this is the point where the specifix go to work.
foreach ($this->configuration['specifics'] as $name => $enabled) {
$names = array_keys($this->job->configuration['specifics']);
foreach ($names as $name) {
if (!in_array($name, $this->validSpecifics)) {
throw new FireflyException(sprintf('"%s" is not a valid class name', $name));
}

View File

@@ -29,6 +29,9 @@ class Roles implements ConfigurationInterface
/** @var ImportJob */
private $job;
/** @var string */
private $warning = '';
/**
* Get the data necessary to show the configuration screen.
*
@@ -46,9 +49,11 @@ class Roles implements ConfigurationInterface
$end = $start + config('csv.example_rows');
// set data:
$roles = $this->getRoles();
asort($roles);
$this->data = [
'examples' => [],
'roles' => $this->getRoles(),
'roles' => $roles,
'total' => 0,
'headers' => $config['has-headers'] ? $reader->fetchOne(0) : [],
];
@@ -68,6 +73,16 @@ class Roles implements ConfigurationInterface
return $this->data;
}
/**
* Return possible warning to user.
*
* @return string
*/
public function getWarningMessage(): string
{
return $this->warning;
}
/**
* @param ImportJob $job
*
@@ -215,7 +230,8 @@ class Roles implements ConfigurationInterface
*/
private function processSpecifics(array $row): array
{
foreach ($this->job->configuration['specifics'] as $name => $enabled) {
$names = array_keys($this->job->configuration['specifics']);
foreach ($names as $name) {
/** @var SpecificInterface $specific */
$specific = app('FireflyIII\Import\Specifics\\' . $name);
$row = $specific->run($row);
@@ -246,6 +262,10 @@ class Roles implements ConfigurationInterface
$config['column-roles-complete'] = true;
$this->job->configuration = $config;
$this->job->save();
$this->warning = '';
}
if ($assigned === 0 || !$hasAmount) {
$this->warning = strval(trans('csv.roles_warning'));
}
return true;

View File

@@ -0,0 +1,71 @@
<?php
/**
* BunqPrerequisites.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\Import\Prerequisites;
use FireflyIII\User;
use Preferences;
/**
* Class BunqPrerequisites
*
* @package FireflyIII\Support\Import\Prerequisites
*/
class BunqPrerequisites implements PrerequisitesInterface
{
/** @var User */
private $user;
/**
* Returns view name that allows user to fill in prerequisites.
*
* @return string
*/
public function getView(): string
{
return 'import.bunq.prerequisites';
}
/**
* Returns any values required for the prerequisites-view.
*
* @return array
*/
public function getViewParameters(): array
{
return [];
}
/**
* Returns if this import method has any special prerequisites such as config
* variables or other things.
*
* @return bool
*/
public function hasPrerequisites(): bool
{
$apiKey = Preferences::getForUser($this->user, 'bunq_api_key', false);
return ($apiKey->data === false);
}
/**
* Set the user for this Prerequisites-routine. Class is expected to implement and save this.
*
* @param User $user
*/
public function setUser(User $user): void
{
$this->user = $user;
return;
}
}

View File

@@ -0,0 +1,47 @@
<?php
/**
* PrerequisitesInterface.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\Import\Prerequisites;
use FireflyIII\User;
interface PrerequisitesInterface
{
/**
* Returns view name that allows user to fill in prerequisites.
*
* @return string
*/
public function getView(): string;
/**
* Returns any values required for the prerequisites-view.
*
* @return array
*/
public function getViewParameters(): array;
/**
* Returns if this import method has any special prerequisites such as config
* variables or other things.
*
* @return bool
*/
public function hasPrerequisites(): bool;
/**
* Set the user for this Prerequisites-routine. Class is expected to implement and save this.
*
* @param User $user
*/
public function setUser(User $user): void;
}

View File

@@ -135,7 +135,7 @@ class Navigation
*
* @return Carbon
*/
public function endOfX(Carbon $theCurrentEnd, string $repeatFreq, Carbon $maxDate = null): Carbon
public function endOfX(Carbon $theCurrentEnd, string $repeatFreq, ?Carbon $maxDate): Carbon
{
$functionMap = [
'1D' => 'endOfDay',

View File

@@ -113,13 +113,12 @@ class Search implements SearchInterface
}
$collector->removeFilter(InternalTransferFilter::class);
$set = $collector->getPaginatedJournals()->getCollection();
$words = $this->words;
Log::debug(sprintf('Found %d journals to check. ', $set->count()));
// Filter transactions that match the given triggers.
$filtered = $set->filter(
function (Transaction $transaction) use ($words) {
function (Transaction $transaction) {
if ($this->matchModifiers($transaction)) {
return $transaction;
@@ -237,7 +236,7 @@ class Search implements SearchInterface
return false;
}
foreach ($needle as $what) {
if (($pos = strpos($haystack, $what)) !== false) {
if (strpos($haystack, $what) !== false) {
return true;
}
}

View File

@@ -181,12 +181,12 @@ class Steam
$modified = is_null($entry->modified) ? '0' : strval($entry->modified);
$foreignModified = is_null($entry->modified_foreign) ? '0' : strval($entry->modified_foreign);
$amount = '0';
if ($currencyId === $entry->transaction_currency_id) {
if ($currencyId === $entry->transaction_currency_id || $currencyId === 0) {
// use normal amount:
$amount = $modified;
}
if ($currencyId === $entry->foreign_currency_id) {
// use normal amount:
// use foreign amount:
$amount = $foreignModified;
}

View File

@@ -147,7 +147,7 @@ class General extends Twig_Extension
protected function balance(): Twig_SimpleFilter
{
return new Twig_SimpleFilter(
'balance', function (Account $account = null): string {
'balance', function (?Account $account): string {
if (is_null($account)) {
return 'NULL';
}

View File

@@ -24,6 +24,7 @@ use FireflyIII\Models\PiggyBank;
use FireflyIII\Models\TransactionType;
use FireflyIII\Repositories\Budget\BudgetRepositoryInterface;
use FireflyIII\Rules\Triggers\TriggerInterface;
use FireflyIII\Services\Password\Verifier;
use FireflyIII\User;
use Google2FA;
use Illuminate\Contracts\Encryption\DecryptException;
@@ -274,6 +275,29 @@ class FireflyValidator extends Validator
return false;
}
/**
* @param $attribute
* @param $value
* @param $parameters
*
* @return bool
*/
public function validateSecurePassword($attribute, $value, $parameters): bool
{
$verify = false;
if (isset($this->data['verify_password'])) {
$verify = intval($this->data['verify_password']) === 1;
}
if ($verify) {
/** @var Verifier $service */
$service = app(Verifier::class);
return $service->validPassword($value);
}
return true;
}
/**
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
* @param $attribute

View File

@@ -46,7 +46,7 @@
}
],
"require": {
"php": ">=7.0.0",
"php": ">=7.1.0",
"ext-intl": "*",
"ext-bcmath": "*",
"ext-mbstring": "*",

300
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"content-hash": "3570b1bf4768d7a7db0667317e82c668",
"content-hash": "527cbfea501cb9d9a9f36637c357f971",
"packages": [
{
"name": "bacon/bacon-qr-code",
@@ -152,21 +152,21 @@
},
{
"name": "doctrine/annotations",
"version": "v1.4.0",
"version": "v1.5.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/annotations.git",
"reference": "54cacc9b81758b14e3ce750f205a393d52339e97"
"reference": "5beebb01b025c94e93686b7a0ed3edae81fe3e7f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/annotations/zipball/54cacc9b81758b14e3ce750f205a393d52339e97",
"reference": "54cacc9b81758b14e3ce750f205a393d52339e97",
"url": "https://api.github.com/repos/doctrine/annotations/zipball/5beebb01b025c94e93686b7a0ed3edae81fe3e7f",
"reference": "5beebb01b025c94e93686b7a0ed3edae81fe3e7f",
"shasum": ""
},
"require": {
"doctrine/lexer": "1.*",
"php": "^5.6 || ^7.0"
"php": "^7.1"
},
"require-dev": {
"doctrine/cache": "1.*",
@@ -175,7 +175,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.4.x-dev"
"dev-master": "1.5.x-dev"
}
},
"autoload": {
@@ -216,37 +216,41 @@
"docblock",
"parser"
],
"time": "2017-02-24T16:22:25+00:00"
"time": "2017-07-22T10:58:02+00:00"
},
{
"name": "doctrine/cache",
"version": "v1.6.2",
"version": "v1.7.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/cache.git",
"reference": "eb152c5100571c7a45470ff2a35095ab3f3b900b"
"reference": "53d9518ffeb019c51d542ff60cb578f076d3ff16"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/cache/zipball/eb152c5100571c7a45470ff2a35095ab3f3b900b",
"reference": "eb152c5100571c7a45470ff2a35095ab3f3b900b",
"url": "https://api.github.com/repos/doctrine/cache/zipball/53d9518ffeb019c51d542ff60cb578f076d3ff16",
"reference": "53d9518ffeb019c51d542ff60cb578f076d3ff16",
"shasum": ""
},
"require": {
"php": "~5.5|~7.0"
"php": "~7.1"
},
"conflict": {
"doctrine/common": ">2.2,<2.4"
},
"require-dev": {
"phpunit/phpunit": "~4.8|~5.0",
"predis/predis": "~1.0",
"satooshi/php-coveralls": "~0.6"
"alcaeus/mongo-php-adapter": "^1.1",
"mongodb/mongodb": "^1.1",
"phpunit/phpunit": "^5.7",
"predis/predis": "~1.0"
},
"suggest": {
"alcaeus/mongo-php-adapter": "Required to use legacy MongoDB driver"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.6.x-dev"
"dev-master": "1.7.x-dev"
}
},
"autoload": {
@@ -286,24 +290,24 @@
"cache",
"caching"
],
"time": "2017-07-22T12:49:21+00:00"
"time": "2017-07-22T13:00:15+00:00"
},
{
"name": "doctrine/collections",
"version": "v1.4.0",
"version": "v1.5.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/collections.git",
"reference": "1a4fb7e902202c33cce8c55989b945612943c2ba"
"reference": "a01ee38fcd999f34d9bfbcee59dbda5105449cbf"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/collections/zipball/1a4fb7e902202c33cce8c55989b945612943c2ba",
"reference": "1a4fb7e902202c33cce8c55989b945612943c2ba",
"url": "https://api.github.com/repos/doctrine/collections/zipball/a01ee38fcd999f34d9bfbcee59dbda5105449cbf",
"reference": "a01ee38fcd999f34d9bfbcee59dbda5105449cbf",
"shasum": ""
},
"require": {
"php": "^5.6 || ^7.0"
"php": "^7.1"
},
"require-dev": {
"doctrine/coding-standard": "~0.1@dev",
@@ -353,20 +357,20 @@
"collections",
"iterator"
],
"time": "2017-01-03T10:49:41+00:00"
"time": "2017-07-22T10:37:32+00:00"
},
{
"name": "doctrine/common",
"version": "v2.7.3",
"version": "v2.8.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/common.git",
"reference": "4acb8f89626baafede6ee5475bc5844096eba8a9"
"reference": "ed349f953d443963c590b008b37b864b8a3c4b21"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/common/zipball/4acb8f89626baafede6ee5475bc5844096eba8a9",
"reference": "4acb8f89626baafede6ee5475bc5844096eba8a9",
"url": "https://api.github.com/repos/doctrine/common/zipball/ed349f953d443963c590b008b37b864b8a3c4b21",
"reference": "ed349f953d443963c590b008b37b864b8a3c4b21",
"shasum": ""
},
"require": {
@@ -375,15 +379,15 @@
"doctrine/collections": "1.*",
"doctrine/inflector": "1.*",
"doctrine/lexer": "1.*",
"php": "~5.6|~7.0"
"php": "~7.1"
},
"require-dev": {
"phpunit/phpunit": "^5.4.6"
"phpunit/phpunit": "^5.7"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.7.x-dev"
"dev-master": "2.8.x-dev"
}
},
"autoload": {
@@ -426,28 +430,30 @@
"persistence",
"spl"
],
"time": "2017-07-22T08:35:12+00:00"
"time": "2017-07-22T09:01:43+00:00"
},
{
"name": "doctrine/dbal",
"version": "v2.5.13",
"version": "v2.6.1",
"source": {
"type": "git",
"url": "https://github.com/doctrine/dbal.git",
"reference": "729340d8d1eec8f01bff708e12e449a3415af873"
"reference": "1a086f853425b1f5349775ce57e45a772d2d2ba5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/729340d8d1eec8f01bff708e12e449a3415af873",
"reference": "729340d8d1eec8f01bff708e12e449a3415af873",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/1a086f853425b1f5349775ce57e45a772d2d2ba5",
"reference": "1a086f853425b1f5349775ce57e45a772d2d2ba5",
"shasum": ""
},
"require": {
"doctrine/common": ">=2.4,<2.8-dev",
"php": ">=5.3.2"
"doctrine/common": "^2.7.1",
"ext-pdo": "*",
"php": "^7.1"
},
"require-dev": {
"phpunit/phpunit": "4.*",
"phpunit/phpunit": "^5.4.6",
"phpunit/phpunit-mock-objects": "!=3.2.4,!=3.2.5",
"symfony/console": "2.*||^3.0"
},
"suggest": {
@@ -459,7 +465,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.5.x-dev"
"dev-master": "2.6.x-dev"
}
},
"autoload": {
@@ -497,37 +503,37 @@
"persistence",
"queryobject"
],
"time": "2017-07-22T20:44:48+00:00"
"time": "2017-07-28T10:40:18+00:00"
},
{
"name": "doctrine/inflector",
"version": "v1.2.0",
"version": "v1.1.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/inflector.git",
"reference": "e11d84c6e018beedd929cff5220969a3c6d1d462"
"reference": "90b2128806bfde671b6952ab8bea493942c1fdae"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/inflector/zipball/e11d84c6e018beedd929cff5220969a3c6d1d462",
"reference": "e11d84c6e018beedd929cff5220969a3c6d1d462",
"url": "https://api.github.com/repos/doctrine/inflector/zipball/90b2128806bfde671b6952ab8bea493942c1fdae",
"reference": "90b2128806bfde671b6952ab8bea493942c1fdae",
"shasum": ""
},
"require": {
"php": "^7.0"
"php": ">=5.3.2"
},
"require-dev": {
"phpunit/phpunit": "^6.2"
"phpunit/phpunit": "4.*"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.2.x-dev"
"dev-master": "1.1.x-dev"
}
},
"autoload": {
"psr-4": {
"Doctrine\\Common\\Inflector\\": "lib/Doctrine/Common/Inflector"
"psr-0": {
"Doctrine\\Common\\Inflector\\": "lib/"
}
},
"notification-url": "https://packagist.org/downloads/",
@@ -564,7 +570,7 @@
"singularize",
"string"
],
"time": "2017-07-22T12:18:28+00:00"
"time": "2015-11-06T14:35:42+00:00"
},
{
"name": "doctrine/lexer",
@@ -664,20 +670,20 @@
},
{
"name": "laravel/framework",
"version": "v5.4.30",
"version": "v5.4.32",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "b9a64955f4278f45ac348a6e000b5ecc85da167a"
"reference": "b8300578d159199b1195413b67318c79068cd24d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/b9a64955f4278f45ac348a6e000b5ecc85da167a",
"reference": "b9a64955f4278f45ac348a6e000b5ecc85da167a",
"url": "https://api.github.com/repos/laravel/framework/zipball/b8300578d159199b1195413b67318c79068cd24d",
"reference": "b8300578d159199b1195413b67318c79068cd24d",
"shasum": ""
},
"require": {
"doctrine/inflector": "~1.0",
"doctrine/inflector": "~1.1.0",
"erusev/parsedown": "~1.6",
"ext-mbstring": "*",
"ext-openssl": "*",
@@ -789,7 +795,7 @@
"framework",
"laravel"
],
"time": "2017-07-19T19:26:19+00:00"
"time": "2017-08-03T12:59:42+00:00"
},
{
"name": "laravelcollective/html",
@@ -847,16 +853,16 @@
},
{
"name": "league/commonmark",
"version": "0.15.4",
"version": "0.15.6",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/commonmark.git",
"reference": "c4c8e6bf99e62d9568875d9fc3ef473fe3e18e0c"
"reference": "91742543c25fecedc84a4883d2919213e04a73b7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/c4c8e6bf99e62d9568875d9fc3ef473fe3e18e0c",
"reference": "c4c8e6bf99e62d9568875d9fc3ef473fe3e18e0c",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/91742543c25fecedc84a4883d2919213e04a73b7",
"reference": "91742543c25fecedc84a4883d2919213e04a73b7",
"shasum": ""
},
"require": {
@@ -869,7 +875,7 @@
"require-dev": {
"cebe/markdown": "~1.0",
"erusev/parsedown": "~1.0",
"jgm/commonmark": "0.27",
"jgm/commonmark": "0.28",
"michelf/php-markdown": "~1.4",
"mikehaertl/php-shellcommand": "~1.2.0",
"phpunit/phpunit": "~4.3|~5.0",
@@ -912,7 +918,7 @@
"markdown",
"parser"
],
"time": "2017-05-09T12:47:53+00:00"
"time": "2017-08-08T11:47:33+00:00"
},
{
"name": "league/csv",
@@ -973,16 +979,16 @@
},
{
"name": "league/flysystem",
"version": "1.0.40",
"version": "1.0.41",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem.git",
"reference": "3828f0b24e2c1918bb362d57a53205d6dc8fde61"
"reference": "f400aa98912c561ba625ea4065031b7a41e5a155"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/3828f0b24e2c1918bb362d57a53205d6dc8fde61",
"reference": "3828f0b24e2c1918bb362d57a53205d6dc8fde61",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/f400aa98912c561ba625ea4065031b7a41e5a155",
"reference": "f400aa98912c561ba625ea4065031b7a41e5a155",
"shasum": ""
},
"require": {
@@ -1003,13 +1009,13 @@
"league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3",
"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-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",
"spatie/flysystem-dropbox": "Allows you to use Dropbox storage"
"spatie/flysystem-dropbox": "Allows you to use Dropbox storage",
"srmklive/flysystem-dropbox-v2": "Allows you to use Dropbox storage for PHP 5 applications"
},
"type": "library",
"extra": {
@@ -1052,7 +1058,7 @@
"sftp",
"storage"
],
"time": "2017-04-28T10:15:08+00:00"
"time": "2017-08-06T17:41:04+00:00"
},
{
"name": "monolog/monolog",
@@ -1387,16 +1393,16 @@
},
{
"name": "ramsey/uuid",
"version": "3.6.1",
"version": "3.7.0",
"source": {
"type": "git",
"url": "https://github.com/ramsey/uuid.git",
"reference": "4ae32dd9ab8860a4bbd750ad269cba7f06f7934e"
"reference": "0ef23d1b10cf1bc576e9d865a7e9c47982c5715e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/4ae32dd9ab8860a4bbd750ad269cba7f06f7934e",
"reference": "4ae32dd9ab8860a4bbd750ad269cba7f06f7934e",
"url": "https://api.github.com/repos/ramsey/uuid/zipball/0ef23d1b10cf1bc576e9d865a7e9c47982c5715e",
"reference": "0ef23d1b10cf1bc576e9d865a7e9c47982c5715e",
"shasum": ""
},
"require": {
@@ -1465,7 +1471,7 @@
"identifier",
"uuid"
],
"time": "2017-03-26T20:37:53+00:00"
"time": "2017-08-04T13:39:04+00:00"
},
{
"name": "rcrowe/twigbridge",
@@ -1636,16 +1642,16 @@
},
{
"name": "symfony/console",
"version": "v3.3.5",
"version": "v3.3.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "a97e45d98c59510f085fa05225a1acb74dfe0546"
"reference": "b0878233cb5c4391347e5495089c7af11b8e6201"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/a97e45d98c59510f085fa05225a1acb74dfe0546",
"reference": "a97e45d98c59510f085fa05225a1acb74dfe0546",
"url": "https://api.github.com/repos/symfony/console/zipball/b0878233cb5c4391347e5495089c7af11b8e6201",
"reference": "b0878233cb5c4391347e5495089c7af11b8e6201",
"shasum": ""
},
"require": {
@@ -1701,7 +1707,7 @@
],
"description": "Symfony Console Component",
"homepage": "https://symfony.com",
"time": "2017-07-03T13:19:36+00:00"
"time": "2017-07-29T21:27:59+00:00"
},
{
"name": "symfony/css-selector",
@@ -1758,16 +1764,16 @@
},
{
"name": "symfony/debug",
"version": "v3.3.5",
"version": "v3.3.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/debug.git",
"reference": "63b85a968486d95ff9542228dc2e4247f16f9743"
"reference": "7c13ae8ce1e2adbbd574fc39de7be498e1284e13"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/debug/zipball/63b85a968486d95ff9542228dc2e4247f16f9743",
"reference": "63b85a968486d95ff9542228dc2e4247f16f9743",
"url": "https://api.github.com/repos/symfony/debug/zipball/7c13ae8ce1e2adbbd574fc39de7be498e1284e13",
"reference": "7c13ae8ce1e2adbbd574fc39de7be498e1284e13",
"shasum": ""
},
"require": {
@@ -1810,11 +1816,11 @@
],
"description": "Symfony Debug Component",
"homepage": "https://symfony.com",
"time": "2017-07-05T13:02:37+00:00"
"time": "2017-07-28T15:27:31+00:00"
},
{
"name": "symfony/event-dispatcher",
"version": "v2.8.25",
"version": "v2.8.26",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
@@ -1874,7 +1880,7 @@
},
{
"name": "symfony/finder",
"version": "v3.3.5",
"version": "v3.3.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
@@ -1923,16 +1929,16 @@
},
{
"name": "symfony/http-foundation",
"version": "v3.3.5",
"version": "v3.3.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
"reference": "e307abe4b79ccbbfdced9b91c132fd128f456bc5"
"reference": "49e8cd2d59a7aa9bfab19e46de680c76e500a031"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/e307abe4b79ccbbfdced9b91c132fd128f456bc5",
"reference": "e307abe4b79ccbbfdced9b91c132fd128f456bc5",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/49e8cd2d59a7aa9bfab19e46de680c76e500a031",
"reference": "49e8cd2d59a7aa9bfab19e46de680c76e500a031",
"shasum": ""
},
"require": {
@@ -1972,7 +1978,7 @@
],
"description": "Symfony HttpFoundation Component",
"homepage": "https://symfony.com",
"time": "2017-07-17T14:07:10+00:00"
"time": "2017-07-21T11:04:46+00:00"
},
{
"name": "symfony/http-kernel",
@@ -2228,7 +2234,7 @@
},
{
"name": "symfony/process",
"version": "v3.3.5",
"version": "v3.3.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
@@ -2277,16 +2283,16 @@
},
{
"name": "symfony/routing",
"version": "v3.3.5",
"version": "v3.3.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/routing.git",
"reference": "dc70bbd0ca7b19259f63cdacc8af370bc32a4728"
"reference": "4aee1a917fd4859ff8b51b9fd1dfb790a5ecfa26"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/routing/zipball/dc70bbd0ca7b19259f63cdacc8af370bc32a4728",
"reference": "dc70bbd0ca7b19259f63cdacc8af370bc32a4728",
"url": "https://api.github.com/repos/symfony/routing/zipball/4aee1a917fd4859ff8b51b9fd1dfb790a5ecfa26",
"reference": "4aee1a917fd4859ff8b51b9fd1dfb790a5ecfa26",
"shasum": ""
},
"require": {
@@ -2351,11 +2357,11 @@
"uri",
"url"
],
"time": "2017-06-24T09:29:48+00:00"
"time": "2017-07-21T17:43:13+00:00"
},
{
"name": "symfony/translation",
"version": "v3.3.5",
"version": "v3.3.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/translation.git",
@@ -2420,16 +2426,16 @@
},
{
"name": "symfony/var-dumper",
"version": "v3.3.5",
"version": "v3.3.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
"reference": "0f32b62d21991700250fed5109b092949007c5b3"
"reference": "b2623bccb969ad595c2090f9be498b74670d0663"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/0f32b62d21991700250fed5109b092949007c5b3",
"reference": "0f32b62d21991700250fed5109b092949007c5b3",
"url": "https://api.github.com/repos/symfony/var-dumper/zipball/b2623bccb969ad595c2090f9be498b74670d0663",
"reference": "b2623bccb969ad595c2090f9be498b74670d0663",
"shasum": ""
},
"require": {
@@ -2484,7 +2490,7 @@
"debug",
"dump"
],
"time": "2017-07-10T14:18:27+00:00"
"time": "2017-07-28T06:06:09+00:00"
},
{
"name": "tijsverkoyen/css-to-inline-styles",
@@ -2869,32 +2875,32 @@
},
{
"name": "doctrine/instantiator",
"version": "1.0.5",
"version": "1.1.0",
"source": {
"type": "git",
"url": "https://github.com/doctrine/instantiator.git",
"reference": "8e884e78f9f0eb1329e445619e04456e64d8051d"
"reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d",
"reference": "8e884e78f9f0eb1329e445619e04456e64d8051d",
"url": "https://api.github.com/repos/doctrine/instantiator/zipball/185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda",
"reference": "185b8868aa9bf7159f5f953ed5afb2d7fcdc3bda",
"shasum": ""
},
"require": {
"php": ">=5.3,<8.0-DEV"
"php": "^7.1"
},
"require-dev": {
"athletic/athletic": "~0.1.8",
"ext-pdo": "*",
"ext-phar": "*",
"phpunit/phpunit": "~4.0",
"squizlabs/php_codesniffer": "~2.0"
"phpunit/phpunit": "^6.2.3",
"squizlabs/php_codesniffer": "^3.0.2"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
"dev-master": "1.2.x-dev"
}
},
"autoload": {
@@ -2919,7 +2925,7 @@
"constructor",
"instantiate"
],
"time": "2015-06-14T21:17:01+00:00"
"time": "2017-07-22T11:58:36+00:00"
},
{
"name": "fzaninotto/faker",
@@ -3210,7 +3216,7 @@
],
"authors": [
{
"name": "Padraic Brady",
"name": "Pádraic Brady",
"email": "padraic.brady@gmail.com",
"homepage": "http://blog.astrumfutura.com"
},
@@ -3334,22 +3340,22 @@
},
{
"name": "phpdocumentor/reflection-docblock",
"version": "3.2.0",
"version": "3.2.2",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
"reference": "46f7e8bb075036c92695b15a1ddb6971c751e585"
"reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/46f7e8bb075036c92695b15a1ddb6971c751e585",
"reference": "46f7e8bb075036c92695b15a1ddb6971c751e585",
"url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/4aada1f93c72c35e22fb1383b47fee43b8f1d157",
"reference": "4aada1f93c72c35e22fb1383b47fee43b8f1d157",
"shasum": ""
},
"require": {
"php": ">=5.5",
"phpdocumentor/reflection-common": "^1.0@dev",
"phpdocumentor/type-resolver": "^0.4.0",
"phpdocumentor/type-resolver": "^0.3.0",
"webmozart/assert": "^1.0"
},
"require-dev": {
@@ -3375,20 +3381,20 @@
}
],
"description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
"time": "2017-07-15T11:38:20+00:00"
"time": "2017-08-08T06:39:58+00:00"
},
{
"name": "phpdocumentor/type-resolver",
"version": "0.4.0",
"version": "0.3.0",
"source": {
"type": "git",
"url": "https://github.com/phpDocumentor/TypeResolver.git",
"reference": "9c977708995954784726e25d0cd1dddf4e65b0f7"
"reference": "fb3933512008d8162b3cdf9e18dba9309b7c3773"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/9c977708995954784726e25d0cd1dddf4e65b0f7",
"reference": "9c977708995954784726e25d0cd1dddf4e65b0f7",
"url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/fb3933512008d8162b3cdf9e18dba9309b7c3773",
"reference": "fb3933512008d8162b3cdf9e18dba9309b7c3773",
"shasum": ""
},
"require": {
@@ -3422,7 +3428,7 @@
"email": "me@mikevanriel.com"
}
],
"time": "2017-07-14T14:27:02+00:00"
"time": "2017-06-03T08:32:36+00:00"
},
{
"name": "phpspec/prophecy",
@@ -3689,29 +3695,29 @@
},
{
"name": "phpunit/php-token-stream",
"version": "1.4.11",
"version": "2.0.0",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-token-stream.git",
"reference": "e03f8f67534427a787e21a385a67ec3ca6978ea7"
"reference": "ecb0b2cdaa0add708fe6f329ef65ae0c5225130b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/e03f8f67534427a787e21a385a67ec3ca6978ea7",
"reference": "e03f8f67534427a787e21a385a67ec3ca6978ea7",
"url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/ecb0b2cdaa0add708fe6f329ef65ae0c5225130b",
"reference": "ecb0b2cdaa0add708fe6f329ef65ae0c5225130b",
"shasum": ""
},
"require": {
"ext-tokenizer": "*",
"php": ">=5.3.3"
"php": "^7.0"
},
"require-dev": {
"phpunit/phpunit": "~4.2"
"phpunit/phpunit": "^6.2.4"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.4-dev"
"dev-master": "2.0-dev"
}
},
"autoload": {
@@ -3734,7 +3740,7 @@
"keywords": [
"tokenizer"
],
"time": "2017-02-27T10:12:30+00:00"
"time": "2017-08-03T14:17:41+00:00"
},
{
"name": "phpunit/phpunit",
@@ -4450,7 +4456,7 @@
},
{
"name": "symfony/class-loader",
"version": "v3.3.5",
"version": "v3.3.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/class-loader.git",
@@ -4506,16 +4512,16 @@
},
{
"name": "symfony/config",
"version": "v3.3.5",
"version": "v3.3.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/config.git",
"reference": "a094618deb9a3fe1c3cf500a796e167d0495a274"
"reference": "54ee12b0dd60f294132cabae6f5da9573d2e5297"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/config/zipball/a094618deb9a3fe1c3cf500a796e167d0495a274",
"reference": "a094618deb9a3fe1c3cf500a796e167d0495a274",
"url": "https://api.github.com/repos/symfony/config/zipball/54ee12b0dd60f294132cabae6f5da9573d2e5297",
"reference": "54ee12b0dd60f294132cabae6f5da9573d2e5297",
"shasum": ""
},
"require": {
@@ -4564,7 +4570,7 @@
],
"description": "Symfony Config Component",
"homepage": "https://symfony.com",
"time": "2017-06-16T12:40:34+00:00"
"time": "2017-07-19T07:37:29+00:00"
},
{
"name": "symfony/dom-crawler",
@@ -4624,7 +4630,7 @@
},
{
"name": "symfony/filesystem",
"version": "v3.3.5",
"version": "v3.3.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
@@ -4673,7 +4679,7 @@
},
{
"name": "symfony/stopwatch",
"version": "v3.3.5",
"version": "v3.3.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/stopwatch.git",
@@ -4722,16 +4728,16 @@
},
{
"name": "symfony/yaml",
"version": "v3.3.5",
"version": "v3.3.6",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "1f93a8d19b8241617f5074a123e282575b821df8"
"reference": "ddc23324e6cfe066f3dd34a37ff494fa80b617ed"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/1f93a8d19b8241617f5074a123e282575b821df8",
"reference": "1f93a8d19b8241617f5074a123e282575b821df8",
"url": "https://api.github.com/repos/symfony/yaml/zipball/ddc23324e6cfe066f3dd34a37ff494fa80b617ed",
"reference": "ddc23324e6cfe066f3dd34a37ff494fa80b617ed",
"shasum": ""
},
"require": {
@@ -4773,7 +4779,7 @@
],
"description": "Symfony Yaml Component",
"homepage": "https://symfony.com",
"time": "2017-06-15T12:58:50+00:00"
"time": "2017-07-23T12:43:26+00:00"
},
{
"name": "webmozart/assert",
@@ -4832,7 +4838,7 @@
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=7.0.0",
"php": ">=7.1.0",
"ext-intl": "*",
"ext-bcmath": "*",
"ext-mbstring": "*",

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
/**
* auth.php
* Copyright (C) 2016 thegrumpydictator@gmail.com

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
/**
* broadcasting.php
* Copyright (C) 2016 thegrumpydictator@gmail.com

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
/**
* cache.php
* Copyright (C) 2016 thegrumpydictator@gmail.com

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
/**
* compile.php
* Copyright (C) 2016 thegrumpydictator@gmail.com

View File

@@ -191,7 +191,7 @@ return [
'mapper' => 'Categories',
],
'tags-comma' => [
'mappable' => true,
'mappable' => false,
'pre-process-map' => true,
'pre-process-mapper' => 'TagsComma',
'field' => 'tags',
@@ -199,7 +199,7 @@ return [
'mapper' => 'Tags',
],
'tags-space' => [
'mappable' => true,
'mappable' => false,
'pre-process-map' => true,
'pre-process-mapper' => 'TagsSpace',
'field' => 'tags',

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
/**
* database.php
* Copyright (C) 2016 thegrumpydictator@gmail.com

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
/**
* filesystems.php
* Copyright (C) 2016 thegrumpydictator@gmail.com

View File

@@ -23,7 +23,7 @@ return [
'is_demo_site' => false,
],
'encryption' => (is_null(env('USE_ENCRYPTION')) || env('USE_ENCRYPTION') === true),
'version' => '4.6.3.1',
'version' => '4.6.4',
'maxUploadSize' => 15242880,
'allowedMimes' => ['image/png', 'image/jpeg', 'application/pdf'],
'list_length' => 10,
@@ -39,6 +39,9 @@ return [
'import_processors' => [
'csv' => 'FireflyIII\Import\FileProcessor\CsvProcessor',
],
'import_pre' => [
'bunq' => 'FireflyIII\Support\Import\Prerequisites\BunqPrerequisites',
],
'default_export_format' => 'csv',
'default_import_format' => 'csv',
'bill_periods' => ['weekly', 'monthly', 'quarterly', 'half-year', 'yearly'],

View File

@@ -99,12 +99,11 @@ return [
'accountStatus' => ['element' => '#accountStatus', 'position' => 'top'],
],
'piggy-banks_create' => [
'name' => ['element' => '#ffInput_name'],
'date' => ['element' => '#ffInput_targetdate'],
'name' => ['element' => '#ffInput_name'],
'date' => ['element' => '#ffInput_targetdate'],
],
'piggy-banks_show' => [
'intro' => [],
'piggyChart' => ['element' => '#piggyChart'],
'piggyDetails' => ['element' => '#piggyDetails'],
'piggyEvents' => ['element' => '#piggyEvents'],
@@ -146,7 +145,7 @@ return [
],
// preferences: index
'preferences_index' => [
'tabs' => ['element' => '.nav-tabs'],
'tabs' => ['element' => '.nav-tabs'],
],
// currencies: index, create
'currencies_index' => [
@@ -154,6 +153,6 @@ return [
'default' => ['element' => '.defaultCurrency'],
],
'currencies_create' => [
'code' => ['element' => '#ffInput_code',],
'code' => ['element' => '#ffInput_code',],
],
];
];

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
/**
* mail.php
* Copyright (C) 2016 thegrumpydictator@gmail.com

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
/**
* queue.php
* Copyright (C) 2016 thegrumpydictator@gmail.com

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
/**
* services.php
* Copyright (C) 2016 thegrumpydictator@gmail.com

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
/**
* This file is part of the TwigBridge package.

View File

@@ -17,11 +17,13 @@ return [
[
'4.3' => 'Make sure you run the migrations and clear your cache. If you need more help, please check Github or the Firefly III website.',
'4.6.3' => 'This will be the last version to require PHP7.0. Future versions will require PHP7.1 minimum.',
'4.6.4' => 'This version of Firefly III requires PHP7.1.'
],
'install' =>
[
'4.3' => 'Welcome to Firefly! Make sure you follow the installation guide. If you need more help, please check Github or the Firefly III website. The installation guide has a FAQ which you should check out as well.',
'4.6.3' => 'This will be the last version to require PHP7.0. Future versions will require PHP7.1 minimum.',
'4.6.4' => 'This version of Firefly III requires PHP7.1.'
],
],
];

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
/**
* 2016_10_09_150037_expand_transactions_table.php
* Copyright (C) 2016 thegrumpydictator@gmail.com

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
/**
* 2016_10_22_075804_changes_for_v410.php
* Copyright (C) 2016 thegrumpydictator@gmail.com

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
/**
* 2016_11_24_210552_changes_for_v420.php
* Copyright (C) 2016 thegrumpydictator@gmail.com

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
/**
* 2016_12_28_203205_changes_for_v431.php
* Copyright (c) 2016 thegrumpydictator@gmail.com

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
@@ -23,6 +25,7 @@ class ChangesForV440 extends Migration
/**
* Run the migrations.
*
* @SuppressWarnings(PHPMD.ShortMethodName)
* @return void
*/
public function up()

View File

@@ -1,4 +1,6 @@
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
@@ -20,6 +22,7 @@ class ChangesForV450 extends Migration
/**
* Run the migrations.
*
* @SuppressWarnings(PHPMD.ShortMethodName)
* @return void
*/
public function up()

View File

@@ -1,4 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ browserconfig.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.
-->
<browserconfig>
<msapplication>
<tile>

View File

@@ -1,7 +1,7 @@
.daterangepicker {
position: absolute;
color: inherit;
background: #fff;
background-color: #fff;
border-radius: 4px;
width: 278px;
padding: 4px;
@@ -115,7 +115,7 @@
border: 1px solid #fff;
padding: 4px;
border-radius: 4px;
background: #fff;
background-color: #fff;
}
.daterangepicker table {
@@ -274,7 +274,7 @@
.ranges li {
font-size: 13px;
background: #f5f5f5;
background-color: #f5f5f5;
border: 1px solid #f5f5f5;
border-radius: 4px;
color: #08c;
@@ -284,13 +284,13 @@
}
.ranges li:hover {
background: #08c;
background-color: #08c;
border: 1px solid #08c;
color: #fff;
}
.ranges li.active {
background: #08c;
background-color: #08c;
border: 1px solid #08c;
color: #fff;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -7,7 +7,7 @@
*
* See the LICENSE file for details.
*/
/** global: Chart, defaultChartOptions, accounting, defaultPieOptions, noDataForChart, noDataForChart */
/** global: Chart, defaultChartOptions, accounting, defaultPieOptions, noDataForChart */
var allCharts = {};
/*

View File

@@ -78,8 +78,7 @@ function showDownload() {
function showError(text) {
"use strict";
$('#export-error').show();
$('#export-error').find('p').text(text);
$('#export-error').show().find('p').text(text);
}
function callExport() {

View File

@@ -7,7 +7,7 @@
*
* See the LICENSE file for details.
*/
/** global: moment, accountingConfig, dateRangeConfig, accounting, currencySymbol, mon_decimal_point, frac_digits, showFullList, showOnlyTop, mon_thousands_sep */
/** global: moment, dateRangeMeta,dateRangeConfig, accountingConfig, accounting, currencySymbol, mon_decimal_point, frac_digits, showFullList, showOnlyTop, mon_thousands_sep */
$(function () {
@@ -16,8 +16,8 @@ $(function () {
configAccounting(currencySymbol);
// on submit of form, disable any button in form:
$('form.form-horizontal').on('submit',function() {
$('button[type="submit"]').prop('disabled',true);
$('form.form-horizontal').on('submit', function () {
$('button[type="submit"]').prop('disabled', true);
});
$.ajaxSetup({
@@ -29,27 +29,18 @@ $(function () {
// when you click on a currency, this happens:
$('.currency-option').on('click', currencySelect);
var ranges = {};
ranges[dateRangeConfig.currentPeriod] = [moment(dateRangeConfig.ranges.current[0]), moment(dateRangeConfig.ranges.current[1])];
ranges[dateRangeConfig.previousPeriod] = [moment(dateRangeConfig.ranges.previous[0]), moment(dateRangeConfig.ranges.previous[1])];
ranges[dateRangeConfig.nextPeriod] = [moment(dateRangeConfig.ranges.next[0]), moment(dateRangeConfig.ranges.next[1])];
// range for everything:
ranges[dateRangeConfig.everything] = [dateRangeConfig.firstDate, moment()];
// build the data range:
$('#daterange').text(dateRangeConfig.linkTitle).daterangepicker(
$('#daterange').text(dateRangeMeta.title).daterangepicker(
{
ranges: ranges,
ranges: dateRangeConfig.ranges,
opens: 'left',
locale: {
applyLabel: dateRangeConfig.applyLabel,
cancelLabel: dateRangeConfig.cancelLabel,
fromLabel: dateRangeConfig.fromLabel,
toLabel: dateRangeConfig.toLabel,
applyLabel: dateRangeMeta.labels.apply,
cancelLabel: dateRangeMeta.labels.cancel,
fromLabel: dateRangeMeta.labels.from,
toLabel: dateRangeMeta.labels.to,
weekLabel: 'W',
customRangeLabel: dateRangeConfig.customRangeLabel,
customRangeLabel: dateRangeMeta.labels.customRange,
daysOfWeek: moment.weekdaysMin(),
monthNames: moment.monthsShort(),
firstDay: moment.localeData()._week.dow
@@ -61,7 +52,7 @@ $(function () {
function (start, end, label) {
// send post.
$.post(dateRangeConfig.URL, {
$.post(dateRangeMeta.uri, {
start: start.format('YYYY-MM-DD'),
end: end.format('YYYY-MM-DD'),
label: label

View File

@@ -60,8 +60,6 @@ function failedJobImport(jqxhr, textStatus, error) {
function reportOnJobImport(data) {
switch (data.status) {
default:
break;
case "configured":
// job is ready. Do not check again, just show the start-box. Hide the rest.
$('.statusbox').hide();
@@ -94,9 +92,10 @@ function reportOnJobImport(data) {
$('.status_finished').show();
// show text:
$('#import-status-more-info').html(data.finishedText);
break;
default:
break;
}
}

View File

@@ -10,7 +10,7 @@
$(function () {
"use strict";
if(!forceDemoOff) {
if (!forceDemoOff) {
$.getJSON(routeStepsUri).done(setupIntro)
}
});

View File

@@ -69,6 +69,7 @@ function removeMoney(e) {
return false;
}
function stopSorting() {
"use strict";
$('.loadSpin').addClass('fa fa-refresh fa-spin');

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