diff --git a/app/Helpers/Csv/Converter/TagsComma.php b/app/Helpers/Csv/Converter/TagsComma.php new file mode 100644 index 0000000000..73ad4aa71f --- /dev/null +++ b/app/Helpers/Csv/Converter/TagsComma.php @@ -0,0 +1,40 @@ +value); + foreach ($strings as $string) { + $tag = Tag::firstOrCreateEncrypted( + [ + 'tag' => $string, + 'tagMode' => 'nothing', + 'user_id' => Auth::user()->id, + ] + ); + $tags->push($tag); + } + $tags = $tags->merge($this->data['tags']); + + return $tags; + } +} \ No newline at end of file diff --git a/app/Helpers/Csv/Converter/TagsSpace.php b/app/Helpers/Csv/Converter/TagsSpace.php new file mode 100644 index 0000000000..6e0353832d --- /dev/null +++ b/app/Helpers/Csv/Converter/TagsSpace.php @@ -0,0 +1,40 @@ +value); + foreach ($strings as $string) { + $tag = Tag::firstOrCreateEncrypted( + [ + 'tag' => $string, + 'tagMode' => 'nothing', + 'user_id' => Auth::user()->id, + ] + ); + $tags->push($tag); + } + $tags = $tags->merge($this->data['tags']); + + return $tags; + } +} \ No newline at end of file diff --git a/app/Helpers/Csv/Importer.php b/app/Helpers/Csv/Importer.php index df16ab374d..83c9f1553b 100644 --- a/app/Helpers/Csv/Importer.php +++ b/app/Helpers/Csv/Importer.php @@ -7,8 +7,6 @@ use Auth; use Config; use FireflyIII\Exceptions\FireflyException; use FireflyIII\Helpers\Csv\Converter\ConverterInterface; -use FireflyIII\Models\Account; -use FireflyIII\Models\AccountType; use FireflyIII\Models\Transaction; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionJournal; @@ -16,7 +14,6 @@ use FireflyIII\Models\TransactionType; use Illuminate\Support\MessageBag; use Log; use Preferences; -use ReflectionException; set_time_limit(0); @@ -75,10 +72,10 @@ class Importer $this->map = $this->data->getMap(); $this->roles = $this->data->getRoles(); $this->mapped = $this->data->getMapped(); + foreach ($this->data->getReader() as $index => $row) { - if (($this->data->getHasHeaders() && $index > 1) || !$this->data->getHasHeaders()) { + if ($this->parseRow($index)) { $this->rows++; - Log::debug('Now at row ' . $index); $result = $this->importRow($row); if (!($result === true)) { Log::error('Caught error at row #' . $index . ': ' . $result); @@ -90,6 +87,16 @@ class Importer } } + /** + * @param int $index + * + * @return bool + */ + protected function parseRow($index) + { + return (($this->data->getHasHeaders() && $index > 1) || !$this->data->getHasHeaders()); + } + /** * @param $row * @@ -107,18 +114,8 @@ class Importer $class = Config::get('csv.roles.' . $role . '.converter'); $field = Config::get('csv.roles.' . $role . '.field'); - if (is_null($class)) { - throw new FireflyException('No converter for field of type "' . $role . '".'); - } - if (is_null($field)) { - throw new FireflyException('No place to store value of type "' . $role . '".'); - } - try { - /** @var ConverterInterface $converter */ - $converter = App::make('FireflyIII\Helpers\Csv\Converter\\' . $class); - } catch (ReflectionException $e) { - throw new FireflyException('Cannot continue with column of type "' . $role . '" because class "' . $class . '" cannot be found.'); - } + /** @var ConverterInterface $converter */ + $converter = App::make('FireflyIII\Helpers\Csv\Converter\\' . $class); $converter->setData($data); // the complete array so far. $converter->setField($field); $converter->setIndex($index); @@ -128,7 +125,8 @@ class Importer $data[$field] = $converter->convert(); } - $data = $this->postProcess($data, $row); + // post processing and validating. + $data = $this->postProcess($data); $result = $this->validateData($data); if ($result === true) { $result = $this->createTransactionJournal($data); @@ -168,37 +166,42 @@ class Importer * Row denotes the original data. * * @param array $data - * @param array $row * * @return array */ - protected function postProcess(array $data, array $row) + protected function postProcess(array $data) { + // fix two simple fields: bcscale(2); $data['description'] = trim($data['description']); $data['amount'] = bcmul($data['amount'], $data['amount-modifier']); - - // get opposing account, which is quite complex: - $data['opposing-account-object'] = $this->processOpposingAccount($data); - if (strlen($data['description']) == 0) { $data['description'] = trans('firefly.csv_empty_description'); } + // fix currency if (is_null($data['currency'])) { $currencyPreference = Preferences::get('currencyPreference', 'EUR'); $data['currency'] = TransactionCurrency::whereCode($currencyPreference->data)->first(); } + + // get bill id. if (!is_null($data['bill'])) { $data['bill-id'] = $data['bill']->id; } + // opposing account can be difficult. + + // get opposing account, which is quite complex: + $opposingAccount = new OpposingAccount($data); + $data['opposing-account-object'] = $opposingAccount->parse(); + // do bank specific fixes: - $specifix = new Specifix(); - $specifix->setData($data); - $specifix->setRow($row); + // $specifix = new Specifix(); + // $specifix->setData($data); + // $specifix->setRow($row); //$specifix->fix($data, $row); // TODO // get data back: //$data = $specifix->getData(); // TODO @@ -206,97 +209,6 @@ class Importer return $data; } - /** - * @param array $data - */ - protected function processOpposingAccount(array $data) - { - // first priority. try to find the account based on ID, - // if any. - if ($data['opposing-account-id'] instanceof Account) { - return $data['opposing-account-id']; - } - - // second: try to find the account based on IBAN, if any. - if ($data['opposing-account-iban'] instanceof Account) { - return $data['opposing-account-iban']; - } - - $accountType = $this->getAccountType($data['amount']); - - if (is_string($data['opposing-account-iban'])) { - $accounts = Auth::user()->accounts()->where('account_type_id', $accountType->id)->get(); - foreach ($accounts as $entry) { - if ($entry->iban == $data['opposing-account-iban']) { - - //return $entry; - } - } - // create if not exists: - $account = Account::firstOrCreateEncrypted( - [ - 'user_id' => Auth::user()->id, - 'account_type_id' => $accountType->id, - 'name' => $data['opposing-account-iban'], - 'iban' => $data['opposing-account-iban'], - 'active' => true, - ] - ); - - return $account; - - } - - // third: try to find account based on name, if any. - if ($data['opposing-account-name'] instanceof Account) { - return $data['opposing-account-name']; - } - - if (is_string($data['opposing-account-name'])) { - $accounts = Auth::user()->accounts()->where('account_type_id', $accountType->id)->get(); - foreach ($accounts as $entry) { - if ($entry->name == $data['opposing-account-name']) { - return $entry; - } - } - // create if not exists: - $account = Account::firstOrCreateEncrypted( - [ - 'user_id' => Auth::user()->id, - 'account_type_id' => $accountType->id, - 'name' => $data['opposing-account-name'], - 'iban' => '', - 'active' => true, - ] - ); - - return $account; - } - return null; - - // if nothing, create expense/revenue, never asset. TODO - - } - - /** - * @param $amount - * - * @return AccountType - */ - protected function getAccountType($amount) - { - // opposing account type: - if ($amount < 0) { - // create expense account: - - return AccountType::where('type', 'Expense account')->first(); - } else { - // create revenue account: - - return AccountType::where('type', 'Revenue account')->first(); - } - } - /** * @param $data * @@ -326,11 +238,17 @@ class Importer if (is_null($data['date'])) { $date = $data['date-rent']; } + + // defaults to deposit + $transactionType = TransactionType::where('type', 'Deposit')->first(); if ($data['amount'] < 0) { $transactionType = TransactionType::where('type', 'Withdrawal')->first(); - } else { - $transactionType = TransactionType::where('type', 'Deposit')->first(); } + + if ($data['opposing-account-object']->accountType->type == 'Asset account') { + $transactionType = TransactionType::where('type', 'Transfer')->first(); + } + $errors = new MessageBag; $journal = TransactionJournal::create( [ @@ -378,6 +296,11 @@ class Importer if (!is_null($data['category'])) { $journal->categories()->save($data['category']); } + if (!is_null($data['tags'])) { + foreach ($data['tags'] as $tag) { + $journal->tags()->save($tag); + } + } return $journal; @@ -391,6 +314,4 @@ class Importer { $this->data = $data; } - - } \ No newline at end of file diff --git a/app/Helpers/Csv/Mapper/AnyAccount.php b/app/Helpers/Csv/Mapper/AnyAccount.php new file mode 100644 index 0000000000..37488c1523 --- /dev/null +++ b/app/Helpers/Csv/Mapper/AnyAccount.php @@ -0,0 +1,32 @@ +accounts()->with('accountType')->orderBy('accounts.name', 'ASC')->get(['accounts.*']); + + $list = []; + /** @var Account $account */ + foreach ($result as $account) { + $list[$account->id] = $account->name . ' (' . $account->accountType->type . ')'; + } + asort($list); + + return $list; + } +} \ No newline at end of file diff --git a/app/Helpers/Csv/Mapper/AssetAccount.php b/app/Helpers/Csv/Mapper/AssetAccount.php index 44982539b5..cb49177976 100644 --- a/app/Helpers/Csv/Mapper/AssetAccount.php +++ b/app/Helpers/Csv/Mapper/AssetAccount.php @@ -31,6 +31,8 @@ class AssetAccount implements MapperInterface $list[$account->id] = $account->name; } + asort($list); + return $list; } } \ No newline at end of file diff --git a/app/Helpers/Csv/Mapper/Bill.php b/app/Helpers/Csv/Mapper/Bill.php new file mode 100644 index 0000000000..17a9f5b22e --- /dev/null +++ b/app/Helpers/Csv/Mapper/Bill.php @@ -0,0 +1,32 @@ +bills()->get(['bills.*']); + $list = []; + + /** @var BillModel $bill */ + foreach ($result as $bill) { + $list[$bill->id] = $bill->name . ' [' . $bill->match . ']'; + } + asort($list); + + return $list; + } +} \ No newline at end of file diff --git a/app/Helpers/Csv/Mapper/Budget.php b/app/Helpers/Csv/Mapper/Budget.php new file mode 100644 index 0000000000..c4bf2064eb --- /dev/null +++ b/app/Helpers/Csv/Mapper/Budget.php @@ -0,0 +1,32 @@ +budgets()->get(['budgets.*']); + $list = []; + + /** @var BudgetModel $budget */ + foreach ($result as $budget) { + $list[$budget->id] = $budget->name; + } + asort($list); + + return $list; + } +} \ No newline at end of file diff --git a/app/Helpers/Csv/Mapper/Category.php b/app/Helpers/Csv/Mapper/Category.php new file mode 100644 index 0000000000..81634764f0 --- /dev/null +++ b/app/Helpers/Csv/Mapper/Category.php @@ -0,0 +1,32 @@ +categories()->get(['categories.*']); + $list = []; + + /** @var CategoryModel $category */ + foreach ($result as $category) { + $list[$category->id] = $category->name; + } + asort($list); + + return $list; + } +} \ No newline at end of file diff --git a/app/Helpers/Csv/Mapper/Tag.php b/app/Helpers/Csv/Mapper/Tag.php new file mode 100644 index 0000000000..9ef0d6a2db --- /dev/null +++ b/app/Helpers/Csv/Mapper/Tag.php @@ -0,0 +1,32 @@ +budgets()->get(['tags.*']); + $list = []; + + /** @var TagModel $tag */ + foreach ($result as $tag) { + $list[$tag->id] = $tag->tag; + } + asort($list); + + return $list; + } +} \ No newline at end of file diff --git a/app/Helpers/Csv/OpposingAccount.php b/app/Helpers/Csv/OpposingAccount.php new file mode 100644 index 0000000000..021423aeca --- /dev/null +++ b/app/Helpers/Csv/OpposingAccount.php @@ -0,0 +1,137 @@ +data = $data; + } + + /** + * @return \FireflyIII\Models\Account|null + */ + public function parse() + { + // first priority. try to find the account based on ID, + // if any. + if ($this->data['opposing-account-id'] instanceof Account) { + + return $this->data['opposing-account-id']; + } + + // second: try to find the account based on IBAN, if any. + if ($this->data['opposing-account-iban'] instanceof Account) { + return $this->data['opposing-account-iban']; + } + + + if (is_string($this->data['opposing-account-iban'])) { + + return $this->parseIbanString(); + } + + // third: try to find account based on name, if any. + if ($this->data['opposing-account-name'] instanceof Account) { + + return $this->data['opposing-account-name']; + } + + if (is_string($this->data['opposing-account-name'])) { + return $this->parseNameString(); + } + + return null; + + // if nothing, create expense/revenue, never asset. TODO + } + + /** + * @return Account|null + */ + protected function parseIbanString() + { + + // create by name and/or iban. + $accountType = $this->getAccountType(); + $accounts = Auth::user()->accounts()->where('account_type_id', $accountType->id)->get(); + foreach ($accounts as $entry) { + if ($entry->iban == $this->data['opposing-account-iban']) { + + return $entry; + } + } + // create if not exists: + $account = Account::firstOrCreateEncrypted( + [ + 'user_id' => Auth::user()->id, + 'account_type_id' => $accountType->id, + 'name' => $this->data['opposing-account-iban'], + 'iban' => $this->data['opposing-account-iban'], + 'active' => true, + ] + ); + + return $account; + } + + /** + * + * @return AccountType + */ + protected function getAccountType() + { + // opposing account type: + if ($this->data['amount'] < 0) { + // create expense account: + + return AccountType::where('type', 'Expense account')->first(); + } else { + // create revenue account: + + return AccountType::where('type', 'Revenue account')->first(); + } + } + + /** + * @return Account|null + */ + protected function parseNameString() + { + $accountType = $this->getAccountType(); + $accounts = Auth::user()->accounts()->where('account_type_id', $accountType->id)->get(); + foreach ($accounts as $entry) { + if ($entry->name == $this->data['opposing-account-name']) { + return $entry; + } + } + // create if not exists: + $account = Account::firstOrCreateEncrypted( + [ + 'user_id' => Auth::user()->id, + 'account_type_id' => $accountType->id, + 'name' => $this->data['opposing-account-name'], + 'iban' => '', + 'active' => true, + ] + ); + + return $account; + } +} \ No newline at end of file diff --git a/app/Http/Controllers/CsvController.php b/app/Http/Controllers/CsvController.php index 1376a64141..68d79c0529 100644 --- a/app/Http/Controllers/CsvController.php +++ b/app/Http/Controllers/CsvController.php @@ -151,6 +151,7 @@ class CsvController extends Controller Session::forget('csv-map'); Session::forget('csv-roles'); Session::forget('csv-mapped'); + Session::forget('csv-specifix'); // get values which are yet unsaveable or unmappable: $unsupported = []; diff --git a/config/csv.php b/config/csv.php index 1fcd459bc6..937b2bbfa8 100644 --- a/config/csv.php +++ b/config/csv.php @@ -11,25 +11,29 @@ return [ 'name' => 'Bill ID (matching Firefly)', 'mappable' => false, 'field' => 'bill', - 'converter' => 'BillId' + 'converter' => 'BillId', + 'mapper' => 'Bill', ], 'bill-name' => [ 'name' => 'Bill name', 'mappable' => true, 'converter' => 'BillName', 'field' => 'bill', + 'mapper' => 'Bill', ], 'currency-id' => [ 'name' => 'Currency ID (matching Firefly)', 'mappable' => true, 'converter' => 'CurrencyId', 'field' => 'currency', + 'mapper' => 'TransactionCurrency' ], 'currency-name' => [ 'name' => 'Currency name (matching Firefly)', 'mappable' => true, 'converter' => 'CurrencyName', 'field' => 'currency', + 'mapper' => 'TransactionCurrency' ], 'currency-code' => [ 'name' => 'Currency code (ISO 4217)', @@ -43,6 +47,7 @@ return [ 'mappable' => true, 'converter' => 'CurrencySymbol', 'field' => 'currency', + 'mapper' => 'TransactionCurrency' ], 'description' => [ 'name' => 'Description', @@ -66,13 +71,15 @@ return [ 'name' => 'Budget ID (matching Firefly)', 'mappable' => true, 'converter' => 'BudgetId', - 'field' => 'budget' + 'field' => 'budget', + 'mapper' => 'Budget', ], 'budget-name' => [ 'name' => 'Budget name', 'mappable' => true, 'converter' => 'BudgetName', - 'field' => 'budget' + 'field' => 'budget', + 'mapper' => 'Budget', ], 'rabo-debet-credit' => [ 'name' => 'Rabobank specific debet/credit indicator', @@ -84,21 +91,29 @@ return [ 'name' => 'Category ID (matching Firefly)', 'mappable' => true, 'converter' => 'CategoryId', - 'field' => 'category' + 'field' => 'category', + 'mapper' => 'Category', ], 'category-name' => [ 'name' => 'Category name', 'mappable' => true, 'converter' => 'CategoryName', - 'field' => 'category' + 'field' => 'category', + 'mapper' => 'Category', ], 'tags-comma' => [ - 'name' => 'Tags (comma separated)', - 'mappable' => true, + 'name' => 'Tags (comma separated)', + 'mappable' => true, + 'field' => 'tags', + 'converter' => 'TagsComma', + 'mapper' => 'Tag', ], 'tags-space' => [ - 'name' => 'Tags (space separated)', - 'mappable' => true, + 'name' => 'Tags (space separated)', + 'mappable' => true, + 'field' => 'tags', + 'converter' => 'TagsSpace', + 'mapper' => 'Tag', ], 'account-id' => [ 'name' => 'Asset account ID (matching Firefly)', @@ -125,19 +140,22 @@ return [ 'name' => 'Opposing account account ID (matching Firefly)', 'mappable' => true, 'field' => 'opposing-account-id', - 'converter' => 'OpposingAccountId' + 'converter' => 'OpposingAccountId', + 'mapper' => 'AnyAccount', ], 'opposing-name' => [ - 'name' => 'Opposing account name', - 'mappable' => true, - 'field' => 'opposing-account-name', - 'converter' => 'OpposingAccountName' + 'name' => 'Opposing account name', + 'mappable' => true, + 'field' => 'opposing-account-name', + 'converter' => 'OpposingAccountName', + 'mapper' => 'AnyAccount', ], 'opposing-iban' => [ - 'name' => 'Opposing account IBAN', - 'mappable' => true, - 'field' => 'opposing-account-iban', - 'converter' => 'OpposingAccountIban' + 'name' => 'Opposing account IBAN', + 'mappable' => true, + 'field' => 'opposing-account-iban', + 'converter' => 'OpposingAccountIban', + 'mapper' => 'AnyAccount', ], 'amount' => [ 'name' => 'Amount',