diff --git a/app/Helpers/Csv/Converter/AccountId.php b/app/Helpers/Csv/Converter/AccountId.php new file mode 100644 index 0000000000..908cd132de --- /dev/null +++ b/app/Helpers/Csv/Converter/AccountId.php @@ -0,0 +1,35 @@ +mapped[$this->index][$this->value]) ? $this->mapped[$this->index][$this->value] : $this->value; + $account = $crud->find($var); + + return $account; + } +} diff --git a/app/Helpers/Csv/Converter/Amount.php b/app/Helpers/Csv/Converter/Amount.php new file mode 100644 index 0000000000..8e2cfc4d0e --- /dev/null +++ b/app/Helpers/Csv/Converter/Amount.php @@ -0,0 +1,32 @@ +value)) { + return strval($this->value); + } + + return '0'; + } +} diff --git a/app/Helpers/Csv/Converter/AmountComma.php b/app/Helpers/Csv/Converter/AmountComma.php new file mode 100644 index 0000000000..8cddad6dc8 --- /dev/null +++ b/app/Helpers/Csv/Converter/AmountComma.php @@ -0,0 +1,36 @@ +value)); + + if (is_numeric($value)) { + return strval($value); + } + + return '0'; + } +} diff --git a/app/Helpers/Csv/Converter/AssetAccountIban.php b/app/Helpers/Csv/Converter/AssetAccountIban.php new file mode 100644 index 0000000000..4da3a73625 --- /dev/null +++ b/app/Helpers/Csv/Converter/AssetAccountIban.php @@ -0,0 +1,89 @@ +mapped[$this->index][$this->value])) { + $account = $crud->find(intval($this->mapped[$this->index][$this->value])); + + return $account; + } + + + if (strlen($this->value) > 0) { + $account = $this->searchOrCreate($crud); + + return $account; + } + + return new Account; + } + + /** + * @param AccountCrudInterface $crud + * + * @return Account + */ + private function searchOrCreate(AccountCrudInterface $crud) + { + // find or create new account: + $set = $crud->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]); + /** @var Account $entry */ + foreach ($set as $entry) { + if ($entry->iban == $this->value) { + + return $entry; + } + } + + + // create it if doesn't exist. + $accountData = [ + 'name' => $this->value, + 'accountType' => 'asset', + 'virtualBalance' => 0, + 'virtualBalanceCurrency' => 1, // hard coded. + 'active' => true, + 'user' => Auth::user()->id, + 'iban' => $this->value, + 'accountNumber' => $this->value, + 'accountRole' => null, + 'openingBalance' => 0, + 'openingBalanceDate' => new Carbon, + 'openingBalanceCurrency' => 1, // hard coded. + ]; + + $account = $crud->store($accountData); + + return $account; + } +} diff --git a/app/Helpers/Csv/Converter/AssetAccountName.php b/app/Helpers/Csv/Converter/AssetAccountName.php new file mode 100644 index 0000000000..e56620415e --- /dev/null +++ b/app/Helpers/Csv/Converter/AssetAccountName.php @@ -0,0 +1,65 @@ +mapped[$this->index][$this->value])) { + $account = $crud->find(intval($this->mapped[$this->index][$this->value])); + + return $account; + } + + $set = $crud->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]); + /** @var Account $entry */ + foreach ($set as $entry) { + if ($entry->name == $this->value) { + return $entry; + } + } + $accountData = [ + 'name' => $this->value, + 'accountType' => 'asset', + 'virtualBalance' => 0, + 'virtualBalanceCurrency' => 1, // hard coded. + 'active' => true, + 'user' => Auth::user()->id, + 'iban' => null, + 'accountNumber' => $this->value, + 'accountRole' => null, + 'openingBalance' => 0, + 'openingBalanceDate' => new Carbon, + 'openingBalanceCurrency' => 1, // hard coded. + ]; + + $account = $crud->store($accountData); + + return $account; + } +} diff --git a/app/Helpers/Csv/Converter/AssetAccountNumber.php b/app/Helpers/Csv/Converter/AssetAccountNumber.php new file mode 100644 index 0000000000..5b8efed50a --- /dev/null +++ b/app/Helpers/Csv/Converter/AssetAccountNumber.php @@ -0,0 +1,77 @@ +mapped[$this->index][$this->value])) { + $account = $crud->find(intval($this->mapped[$this->index][$this->value])); + + return $account; + } + // if not, search for it (or create it): + $value = $this->value ?? ''; + if (strlen($value) > 0) { + // find or create new account: + $set = $crud->getAccountsByType([AccountType::DEFAULT, AccountType::ASSET]); + /** @var Account $entry */ + foreach ($set as $entry) { + $accountNumber = $entry->getMeta('accountNumber'); + if ($accountNumber == $this->value) { + + return $entry; + } + } + + $accountData = [ + 'name' => $this->value, + 'accountType' => 'asset', + 'virtualBalance' => 0, + 'virtualBalanceCurrency' => 1, // hard coded. + 'active' => true, + 'user' => Auth::user()->id, + 'iban' => null, + 'accountNumber' => $this->value, + 'accountRole' => null, + 'openingBalance' => 0, + 'openingBalanceDate' => new Carbon, + 'openingBalanceCurrency' => 1, // hard coded. + + ]; + + $account = $crud->store($accountData); + + return $account; + } + + return null; // is this accepted? + } + +} diff --git a/app/Helpers/Csv/Converter/BasicConverter.php b/app/Helpers/Csv/Converter/BasicConverter.php new file mode 100644 index 0000000000..63e5f8bb96 --- /dev/null +++ b/app/Helpers/Csv/Converter/BasicConverter.php @@ -0,0 +1,114 @@ +data; + } + + /** + * @param array $data + */ + public function setData(array $data) + { + $this->data = $data; + } + + /** + * @return string + */ + public function getField(): string + { + return $this->field; + } + + /** + * @param string $field + */ + public function setField(string $field) + { + $this->field = $field; + } + + /** + * @return int + */ + public function getIndex(): int + { + return $this->index; + } + + /** + * @param int $index + */ + public function setIndex(int $index) + { + $this->index = $index; + } + + /** + * @return array + */ + public function getMapped(): array + { + return $this->mapped; + } + + /** + * @param array $mapped + */ + public function setMapped(array $mapped) + { + $this->mapped = $mapped; + } + + /** + * @return string + */ + public function getValue(): string + { + return $this->value; + } + + /** + * @param string $value + */ + public function setValue(string $value) + { + $this->value = $value; + } + + +} diff --git a/app/Helpers/Csv/Converter/BillId.php b/app/Helpers/Csv/Converter/BillId.php new file mode 100644 index 0000000000..3e9102ccec --- /dev/null +++ b/app/Helpers/Csv/Converter/BillId.php @@ -0,0 +1,36 @@ +mapped[$this->index][$this->value]) ? $this->mapped[$this->index][$this->value] : $this->value; + $bill = $repository->find($value); + + return $bill; + } +} diff --git a/app/Helpers/Csv/Converter/BillName.php b/app/Helpers/Csv/Converter/BillName.php new file mode 100644 index 0000000000..a1328ef6c1 --- /dev/null +++ b/app/Helpers/Csv/Converter/BillName.php @@ -0,0 +1,47 @@ +mapped[$this->index][$this->value])) { + return $repository->find($this->mapped[$this->index][$this->value]); + } + $bills = $repository->getBills(); + + /** @var Bill $bill */ + foreach ($bills as $bill) { + if ($bill->name == $this->value) { + return $bill; + } + } + + return new Bill; + } +} diff --git a/app/Helpers/Csv/Converter/BudgetId.php b/app/Helpers/Csv/Converter/BudgetId.php new file mode 100644 index 0000000000..d9302d7734 --- /dev/null +++ b/app/Helpers/Csv/Converter/BudgetId.php @@ -0,0 +1,36 @@ +mapped[$this->index][$this->value]) ? $this->mapped[$this->index][$this->value] : $this->value; + $budget = $repository->find($value); + + return $budget; + } +} diff --git a/app/Helpers/Csv/Converter/BudgetName.php b/app/Helpers/Csv/Converter/BudgetName.php new file mode 100644 index 0000000000..84f777775b --- /dev/null +++ b/app/Helpers/Csv/Converter/BudgetName.php @@ -0,0 +1,44 @@ +mapped[$this->index][$this->value])) { + $budget = $repository->find($this->mapped[$this->index][$this->value]); + + return $budget; + } + $budget = $repository->store(['name' => $this->value, 'user' => Auth::user()->id]); + + + return $budget; + } +} diff --git a/app/Helpers/Csv/Converter/CategoryId.php b/app/Helpers/Csv/Converter/CategoryId.php new file mode 100644 index 0000000000..8ffedd9ae1 --- /dev/null +++ b/app/Helpers/Csv/Converter/CategoryId.php @@ -0,0 +1,36 @@ +mapped[$this->index][$this->value]) ? $this->mapped[$this->index][$this->value] : $this->value; + $category = $repository->find($value); + + return $category; + } +} diff --git a/app/Helpers/Csv/Converter/CategoryName.php b/app/Helpers/Csv/Converter/CategoryName.php new file mode 100644 index 0000000000..d322f7b02f --- /dev/null +++ b/app/Helpers/Csv/Converter/CategoryName.php @@ -0,0 +1,49 @@ +mapped[$this->index][$this->value])) { + $category = $repository->find($this->mapped[$this->index][$this->value]); + + return $category; + } + + $data = [ + 'name' => $this->value, + 'user' => Auth::user()->id, + ]; + + $category = $repository->store($data); + + return $category; + } +} diff --git a/app/Helpers/Csv/Converter/ConverterInterface.php b/app/Helpers/Csv/Converter/ConverterInterface.php new file mode 100644 index 0000000000..c8fb526b83 --- /dev/null +++ b/app/Helpers/Csv/Converter/ConverterInterface.php @@ -0,0 +1,52 @@ +mapped[$this->index][$this->value])) { + $currency = $repository->find(intval($this->mapped[$this->index][$this->value])); + + return $currency; + } + + $currency = $repository->findByCode($this->value); + + + return $currency; + } +} diff --git a/app/Helpers/Csv/Converter/CurrencyId.php b/app/Helpers/Csv/Converter/CurrencyId.php new file mode 100644 index 0000000000..a18082872a --- /dev/null +++ b/app/Helpers/Csv/Converter/CurrencyId.php @@ -0,0 +1,36 @@ +mapped[$this->index][$this->value]) ? $this->mapped[$this->index][$this->value] : $this->value; + $currency = $repository->find($value); + + return $currency; + } +} diff --git a/app/Helpers/Csv/Converter/CurrencyName.php b/app/Helpers/Csv/Converter/CurrencyName.php new file mode 100644 index 0000000000..62876fa9ba --- /dev/null +++ b/app/Helpers/Csv/Converter/CurrencyName.php @@ -0,0 +1,42 @@ +mapped[$this->index][$this->value])) { + $currency = $repository->find($this->mapped[$this->index][$this->value]); + + return $currency; + } + $currency = $repository->findByName($this->value); + + + return $currency; + } +} diff --git a/app/Helpers/Csv/Converter/CurrencySymbol.php b/app/Helpers/Csv/Converter/CurrencySymbol.php new file mode 100644 index 0000000000..51037ece2e --- /dev/null +++ b/app/Helpers/Csv/Converter/CurrencySymbol.php @@ -0,0 +1,43 @@ +mapped[$this->index][$this->value])) { + $currency = $repository->find($this->mapped[$this->index][$this->value]); + + return $currency; + } + + $currency = $repository->findBySymbol($this->value); + + + return $currency; + } +} diff --git a/app/Helpers/Csv/Converter/Date.php b/app/Helpers/Csv/Converter/Date.php new file mode 100644 index 0000000000..b164e9df5f --- /dev/null +++ b/app/Helpers/Csv/Converter/Date.php @@ -0,0 +1,46 @@ +value); + } catch (InvalidArgumentException $e) { + Log::error('Date conversion error: ' . $e->getMessage() . '. Value was "' . $this->value . '", format was "' . $format . '".'); + + $message = trans('firefly.csv_date_parse_error', ['format' => $format, 'value' => $this->value]); + + throw new FireflyException($message); + } + + + return $date; + } +} diff --git a/app/Helpers/Csv/Converter/Description.php b/app/Helpers/Csv/Converter/Description.php new file mode 100644 index 0000000000..ea864a3e3e --- /dev/null +++ b/app/Helpers/Csv/Converter/Description.php @@ -0,0 +1,31 @@ +data['description'] ?? ''; + + return trim($description . ' ' . $this->value); + } +} diff --git a/app/Helpers/Csv/Converter/INGDebetCredit.php b/app/Helpers/Csv/Converter/INGDebetCredit.php new file mode 100644 index 0000000000..59fd76248a --- /dev/null +++ b/app/Helpers/Csv/Converter/INGDebetCredit.php @@ -0,0 +1,34 @@ +value === 'Af') { + return -1; + } + + return 1; + } +} diff --git a/app/Helpers/Csv/Converter/Ignore.php b/app/Helpers/Csv/Converter/Ignore.php new file mode 100644 index 0000000000..c2da8c3033 --- /dev/null +++ b/app/Helpers/Csv/Converter/Ignore.php @@ -0,0 +1,28 @@ +mapped[$this->index][$this->value])) { + $account = $crud->find($this->mapped[$this->index][$this->value]); + + return $account; + } + + return $this->findAccount($crud); + } + + /** + * @param AccountCrudInterface $crud + * + * @return Account|string + */ + private function findAccount(AccountCrudInterface $crud) + { + if (strlen($this->value) > 0) { + + $set = $crud->getAccountsByType([]); + /** @var Account $account */ + foreach ($set as $account) { + if ($account->iban == $this->value) { + + return $account; + } + } + } + + return $this->value; + } + +} diff --git a/app/Helpers/Csv/Converter/OpposingAccountId.php b/app/Helpers/Csv/Converter/OpposingAccountId.php new file mode 100644 index 0000000000..17836907bf --- /dev/null +++ b/app/Helpers/Csv/Converter/OpposingAccountId.php @@ -0,0 +1,35 @@ +mapped[$this->index][$this->value]) ? $this->mapped[$this->index][$this->value] : $this->value; + $account = $crud->find($value); + + return $account; + } +} diff --git a/app/Helpers/Csv/Converter/OpposingAccountName.php b/app/Helpers/Csv/Converter/OpposingAccountName.php new file mode 100644 index 0000000000..7c3d392985 --- /dev/null +++ b/app/Helpers/Csv/Converter/OpposingAccountName.php @@ -0,0 +1,41 @@ +mapped[$this->index][$this->value])) { + $account = $crud->find($this->mapped[$this->index][$this->value]); + + return $account; + } + + return $this->value; + + } +} diff --git a/app/Helpers/Csv/Converter/RabobankDebetCredit.php b/app/Helpers/Csv/Converter/RabobankDebetCredit.php new file mode 100644 index 0000000000..854f812d45 --- /dev/null +++ b/app/Helpers/Csv/Converter/RabobankDebetCredit.php @@ -0,0 +1,34 @@ +value == 'D') { + return -1; + } + + return 1; + } +} diff --git a/app/Helpers/Csv/Converter/TagsComma.php b/app/Helpers/Csv/Converter/TagsComma.php new file mode 100644 index 0000000000..c9477e7ada --- /dev/null +++ b/app/Helpers/Csv/Converter/TagsComma.php @@ -0,0 +1,53 @@ +value); + foreach ($strings as $string) { + $data = [ + 'tag' => $string, + 'date' => null, + 'description' => null, + 'latitude' => null, + 'longitude' => null, + 'zoomLevel' => null, + 'tagMode' => 'nothing', + ]; + if (strlen($string) > 0) { + $tag = $repository->store($data); // should validate first? + $tags->push($tag); + } + } + $tags = $tags->merge($this->data['tags']); + + return $tags; + } +} diff --git a/app/Helpers/Csv/Converter/TagsSpace.php b/app/Helpers/Csv/Converter/TagsSpace.php new file mode 100644 index 0000000000..aa2694a330 --- /dev/null +++ b/app/Helpers/Csv/Converter/TagsSpace.php @@ -0,0 +1,54 @@ +value); + foreach ($strings as $string) { + $data = [ + 'tag' => $string, + 'date' => null, + 'description' => null, + 'latitude' => null, + 'longitude' => null, + 'zoomLevel' => null, + 'tagMode' => 'nothing', + ]; + if (strlen($string) > 0) { + $tag = $repository->store($data); // should validate first? + $tags->push($tag); + } + } + $tags = $tags->merge($this->data['tags']); + + return $tags; + } +} diff --git a/app/Helpers/Csv/Data.php b/app/Helpers/Csv/Data.php new file mode 100644 index 0000000000..d56f790204 --- /dev/null +++ b/app/Helpers/Csv/Data.php @@ -0,0 +1,336 @@ +sessionHasHeaders(); + $this->sessionDateFormat(); + $this->sessionCsvFileLocation(); + $this->sessionMap(); + $this->sessionRoles(); + $this->sessionMapped(); + $this->sessionSpecifix(); + $this->sessionImportAccount(); + $this->sessionDelimiter(); + } + + /** + * + * @return string + */ + public function getCsvFileContent(): string + { + return $this->csvFileContent ?? ''; + } + + /** + * + * @param string $csvFileContent + */ + public function setCsvFileContent(string $csvFileContent) + { + $this->csvFileContent = $csvFileContent; + } + + /** + * FIXxME may return null + * + * @return string + */ + public function getCsvFileLocation(): string + { + return $this->csvFileLocation; + } + + /** + * + * @param string $csvFileLocation + */ + public function setCsvFileLocation(string $csvFileLocation) + { + Session::put('csv-file', $csvFileLocation); + $this->csvFileLocation = $csvFileLocation; + } + + /** + * FIXxME may return null + * + * @return string + */ + public function getDateFormat(): string + { + return $this->dateFormat; + } + + /** + * + * @param string $dateFormat + */ + public function setDateFormat(string $dateFormat) + { + Session::put('csv-date-format', $dateFormat); + $this->dateFormat = $dateFormat; + } + + /** + * FIXxME may return null + * + * @return string + */ + public function getDelimiter(): string + { + return $this->delimiter; + } + + /** + * + * @param string $delimiter + */ + public function setDelimiter(string $delimiter) + { + Session::put('csv-delimiter', $delimiter); + $this->delimiter = $delimiter; + } + + /** + * + * @return array + */ + public function getMap(): array + { + return $this->map; + } + + /** + * + * @param array $map + */ + public function setMap(array $map) + { + Session::put('csv-map', $map); + $this->map = $map; + } + + /** + * + * @return array + */ + public function getMapped(): array + { + return $this->mapped; + } + + /** + * + * @param array $mapped + */ + public function setMapped(array $mapped) + { + Session::put('csv-mapped', $mapped); + $this->mapped = $mapped; + } + + /** + * + * @return Reader + */ + public function getReader(): Reader + { + if (!is_null($this->csvFileContent) && strlen($this->csvFileContent) === 0) { + $this->loadCsvFile(); + } + + if (is_null($this->reader)) { + $this->reader = Reader::createFromString($this->getCsvFileContent()); + $this->reader->setDelimiter($this->delimiter); + } + + return $this->reader; + } + + /** + * + * @return array + */ + public function getRoles(): array + { + return $this->roles; + } + + /** + * + * @param array $roles + */ + public function setRoles(array $roles) + { + Session::put('csv-roles', $roles); + $this->roles = $roles; + } + + /** + * + * @return array + */ + public function getSpecifix(): array + { + return is_array($this->specifix) ? $this->specifix : []; + } + + /** + * + * @param array $specifix + */ + public function setSpecifix(array $specifix) + { + Session::put('csv-specifix', $specifix); + $this->specifix = $specifix; + } + + /** + * + * @return bool + */ + public function hasHeaders(): bool + { + return $this->hasHeaders; + } + + /** + * + * @param bool $hasHeaders + */ + public function setHasHeaders(bool $hasHeaders) + { + Session::put('csv-has-headers', $hasHeaders); + $this->hasHeaders = $hasHeaders; + } + + /** + * + * @param int $importAccount + */ + public function setImportAccount(int $importAccount) + { + Session::put('csv-import-account', $importAccount); + $this->importAccount = $importAccount; + } + + protected function loadCsvFile() + { + $file = $this->getCsvFileLocation(); + $disk = Storage::disk('upload'); + $content = $disk->get($file); + $contentDecrypted = Crypt::decrypt($content); + $this->setCsvFileContent($contentDecrypted); + } + + protected function sessionCsvFileLocation() + { + if (Session::has('csv-file')) { + $this->csvFileLocation = (string)session('csv-file'); + } + } + + protected function sessionDateFormat() + { + if (Session::has('csv-date-format')) { + $this->dateFormat = (string)session('csv-date-format'); + } + } + + protected function sessionDelimiter() + { + if (Session::has('csv-delimiter')) { + $this->delimiter = session('csv-delimiter'); + } + } + + protected function sessionHasHeaders() + { + if (Session::has('csv-has-headers')) { + $this->hasHeaders = (bool)session('csv-has-headers'); + } + } + + protected function sessionImportAccount() + { + if (Session::has('csv-import-account')) { + $this->importAccount = intval(session('csv-import-account')); + } + } + + protected function sessionMap() + { + if (Session::has('csv-map')) { + $this->map = (array)session('csv-map'); + } + } + + protected function sessionMapped() + { + if (Session::has('csv-mapped')) { + $this->mapped = (array)session('csv-mapped'); + } + } + + protected function sessionRoles() + { + if (Session::has('csv-roles')) { + $this->roles = (array)session('csv-roles'); + } + } + + protected function sessionSpecifix() + { + if (Session::has('csv-specifix')) { + $this->specifix = (array)session('csv-specifix'); + } + } +} diff --git a/app/Helpers/Csv/Importer.php b/app/Helpers/Csv/Importer.php new file mode 100644 index 0000000000..9e50f02a46 --- /dev/null +++ b/app/Helpers/Csv/Importer.php @@ -0,0 +1,384 @@ +errors; + } + + /** + * Used by CsvController + * + * @return int + */ + public function getImported(): int + { + return $this->imported; + } + + /** + * @return Collection + */ + public function getJournals(): Collection + { + return $this->journals; + } + + /** + * Used by CsvController + * + * @return int + */ + public function getRows(): int + { + return $this->rows; + } + + /** + * @return array + */ + public function getSpecifix(): array + { + return is_array($this->specifix) ? $this->specifix : []; + } + + /** + * @throws FireflyException + */ + public function run() + { + set_time_limit(0); + + $this->journals = new Collection; + $this->map = $this->data->getMap(); + $this->roles = $this->data->getRoles(); + $this->mapped = $this->data->getMapped(); + $this->specifix = $this->data->getSpecifix(); + + foreach ($this->data->getReader() as $index => $row) { + if ($this->parseRow($index)) { + $this->rows++; + $result = $this->importRow($row); + if (!($result instanceof TransactionJournal)) { + Log::error('Caught error at row #' . $index . ': ' . $result); + $this->errors[$index] = $result; + } else { + $this->imported++; + $this->journals->push($result); + event(new TransactionJournalStored($result, 0)); + } + } + } + } + + /** + * @param Data $data + */ + public function setData(Data $data) + { + $this->data = $data; + } + + /** + * @return TransactionJournal|string + */ + protected function createTransactionJournal() + { + $date = $this->importData['date']; + if (is_null($this->importData['date'])) { + $date = $this->importData['date-rent']; + } + + + $transactionType = $this->getTransactionType(); // defaults to deposit + $errors = new MessageBag; + $journal = TransactionJournal::create( + [ + 'user_id' => Auth::user()->id, + 'transaction_type_id' => $transactionType->id, + 'transaction_currency_id' => $this->importData['currency']->id, + 'description' => $this->importData['description'], + 'completed' => 0, + 'date' => $date, + 'bill_id' => $this->importData['bill-id'], + ] + ); + if ($journal->getErrors()->count() == 0) { + // first transaction + $accountId = $this->importData['asset-account-object']->id; // create first transaction: + $amount = $this->importData['amount']; + $transaction = Transaction::create(['transaction_journal_id' => $journal->id, 'account_id' => $accountId, 'amount' => $amount]); + $errors = $transaction->getErrors(); + + // second transaction + $accountId = $this->importData['opposing-account-object']->id; // create second transaction: + $amount = bcmul($this->importData['amount'], '-1'); + $transaction = Transaction::create(['transaction_journal_id' => $journal->id, 'account_id' => $accountId, 'amount' => $amount]); + $errors = $transaction->getErrors()->merge($errors); + } + if ($errors->count() == 0) { + $journal->completed = 1; + $journal->save(); + } else { + $text = join(',', $errors->all()); + + return $text; + } + $this->saveBudget($journal); + $this->saveCategory($journal); + $this->saveTags($journal); + + // some debug info: + $journalId = $journal->id; + $type = $journal->transaction_type_type ?? $journal->transactionType->type; + /** @var Account $asset */ + $asset = $this->importData['asset-account-object']; + /** @var Account $opposing */ + $opposing = $this->importData['opposing-account-object']; + + Log::info('Created journal #' . $journalId . ' of type ' . $type . '!'); + Log::info('Asset account #' . $asset->id . ' lost/gained: ' . $this->importData['amount']); + Log::info($opposing->accountType->type . ' #' . $opposing->id . ' lost/gained: ' . bcmul($this->importData['amount'], '-1')); + + return $journal; + } + + /** + * @return TransactionType + */ + protected function getTransactionType() + { + $transactionType = TransactionType::where('type', TransactionType::DEPOSIT)->first(); + if ($this->importData['amount'] < 0) { + $transactionType = TransactionType::where('type', TransactionType::WITHDRAWAL)->first(); + } + + if (in_array($this->importData['opposing-account-object']->accountType->type, ['Asset account', 'Default account'])) { + $transactionType = TransactionType::where('type', TransactionType::TRANSFER)->first(); + } + + return $transactionType; + } + + /** + * @param array $row + * + * @throws FireflyException + * @return string|bool + */ + protected function importRow(array $row) + { + + $data = $this->getFiller(); // These fields are necessary to create a new transaction journal. Some are optional + foreach ($row as $index => $value) { + $role = $this->roles[$index] ?? '_ignore'; + $class = config('csv.roles.' . $role . '.converter'); + $field = config('csv.roles.' . $role . '.field'); + + + // here would be the place where preprocessors would fire. + + /** @var ConverterInterface $converter */ + $converter = app('FireflyIII\Helpers\Csv\Converter\\' . $class); + $converter->setData($data); // the complete array so far. + $converter->setField($field); + $converter->setIndex($index); + $converter->setMapped($this->mapped); + $converter->setValue($value); + $data[$field] = $converter->convert(); + } + // move to class vars. + $this->importData = $data; + $this->importRow = $row; + unset($data, $row); + // post processing and validating. + $this->postProcess(); + $result = $this->validateData(); + + if (!($result === true)) { + return $result; // return error. + } + $journal = $this->createTransactionJournal(); + + return $journal; + } + + /** + * @param int $index + * + * @return bool + */ + protected function parseRow(int $index) + { + return (($this->data->hasHeaders() && $index >= 1) || !$this->data->hasHeaders()); + } + + /** + * Row denotes the original data. + * + * @return void + */ + protected function postProcess() + { + // do bank specific fixes (must be enabled but now all of them. + + foreach ($this->getSpecifix() as $className) { + /** @var SpecifixInterface $specifix */ + $specifix = app('FireflyIII\Helpers\Csv\Specifix\\' . $className); + if ($specifix->getProcessorType() == SpecifixInterface::POST_PROCESSOR) { + $specifix->setData($this->importData); + $specifix->setRow($this->importRow); + $this->importData = $specifix->fix(); + } + } + + + $set = config('csv.post_processors'); + foreach ($set as $className) { + /** @var PostProcessorInterface $postProcessor */ + $postProcessor = app('FireflyIII\Helpers\Csv\PostProcessing\\' . $className); + $array = $this->importData ?? []; + $postProcessor->setData($array); + $this->importData = $postProcessor->process(); + } + + } + + /** + * @param TransactionJournal $journal + */ + protected function saveBudget(TransactionJournal $journal) + { + // add budget: + if (!is_null($this->importData['budget'])) { + $journal->budgets()->save($this->importData['budget']); + } + } + + /** + * @param TransactionJournal $journal + */ + protected function saveCategory(TransactionJournal $journal) + { + // add category: + if (!is_null($this->importData['category'])) { + $journal->categories()->save($this->importData['category']); + } + } + + /** + * @param TransactionJournal $journal + */ + protected function saveTags(TransactionJournal $journal) + { + if (!is_null($this->importData['tags'])) { + foreach ($this->importData['tags'] as $tag) { + $journal->tags()->save($tag); + } + } + } + + /** + * + * @return bool|string + */ + protected function validateData() + { + $date = $this->importData['date'] ?? null; + $rentDate = $this->importData['date-rent'] ?? null; + if (is_null($date) && is_null($rentDate)) { + return 'No date value for this row.'; + } + if (is_null($this->importData['opposing-account-object'])) { + return 'Opposing account is null'; + } + + if (!($this->importData['asset-account-object'] instanceof Account)) { + return 'No asset account to import into.'; + } + + return true; + } + + /** + * @return array + */ + private function getFiller() + { + $filler = []; + foreach (config('csv.roles') as $role) { + if (isset($role['field'])) { + $fieldName = $role['field']; + $filler[$fieldName] = null; + } + } + // some extra's: + $filler['bill-id'] = null; + $filler['opposing-account-object'] = null; + $filler['asset-account-object'] = null; + $filler['amount-modifier'] = '1'; + + return $filler; + + } + +} diff --git a/app/Helpers/Csv/Mapper/AnyAccount.php b/app/Helpers/Csv/Mapper/AnyAccount.php new file mode 100644 index 0000000000..05c5bab9ea --- /dev/null +++ b/app/Helpers/Csv/Mapper/AnyAccount.php @@ -0,0 +1,42 @@ +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); + + $list = [0 => trans('firefly.csv_do_not_map')] + $list; + + return $list; + } +} diff --git a/app/Helpers/Csv/Mapper/AssetAccount.php b/app/Helpers/Csv/Mapper/AssetAccount.php new file mode 100644 index 0000000000..8f32bbad57 --- /dev/null +++ b/app/Helpers/Csv/Mapper/AssetAccount.php @@ -0,0 +1,54 @@ +accounts()->with( + ['accountmeta' => function (HasMany $query) { + $query->where('name', 'accountRole'); + }] + )->accountTypeIn(['Default account', 'Asset account'])->orderBy('accounts.name', 'ASC')->get(['accounts.*']); + + $list = []; + + /** @var Account $account */ + foreach ($result as $account) { + $name = $account->name; + $iban = $account->iban ?? ''; + if (strlen($iban) > 0) { + $name .= ' (' . $account->iban . ')'; + } + $list[$account->id] = $name; + } + + asort($list); + + $list = [0 => trans('firefly.csv_do_not_map')] + $list; + + return $list; + } +} diff --git a/app/Helpers/Csv/Mapper/Bill.php b/app/Helpers/Csv/Mapper/Bill.php new file mode 100644 index 0000000000..37946bf394 --- /dev/null +++ b/app/Helpers/Csv/Mapper/Bill.php @@ -0,0 +1,42 @@ +bills()->get(['bills.*']); + $list = []; + + /** @var BillModel $bill */ + foreach ($result as $bill) { + $list[$bill->id] = $bill->name . ' [' . $bill->match . ']'; + } + asort($list); + + $list = [0 => trans('firefly.csv_do_not_map')] + $list; + + return $list; + } +} diff --git a/app/Helpers/Csv/Mapper/Budget.php b/app/Helpers/Csv/Mapper/Budget.php new file mode 100644 index 0000000000..7664cc3378 --- /dev/null +++ b/app/Helpers/Csv/Mapper/Budget.php @@ -0,0 +1,42 @@ +budgets()->get(['budgets.*']); + $list = []; + + /** @var BudgetModel $budget */ + foreach ($result as $budget) { + $list[$budget->id] = $budget->name; + } + asort($list); + + $list = [0 => trans('firefly.csv_do_not_map')] + $list; + + return $list; + } +} diff --git a/app/Helpers/Csv/Mapper/Category.php b/app/Helpers/Csv/Mapper/Category.php new file mode 100644 index 0000000000..d0a104523a --- /dev/null +++ b/app/Helpers/Csv/Mapper/Category.php @@ -0,0 +1,42 @@ +categories()->get(['categories.*']); + $list = []; + + /** @var CategoryModel $category */ + foreach ($result as $category) { + $list[$category->id] = $category->name; + } + asort($list); + + $list = [0 => trans('firefly.csv_do_not_map')] + $list; + + return $list; + } +} diff --git a/app/Helpers/Csv/Mapper/MapperInterface.php b/app/Helpers/Csv/Mapper/MapperInterface.php new file mode 100644 index 0000000000..eb6286d4f2 --- /dev/null +++ b/app/Helpers/Csv/Mapper/MapperInterface.php @@ -0,0 +1,24 @@ +budgets()->get(['tags.*']); + $list = []; + + /** @var TagModel $tag */ + foreach ($result as $tag) { + $list[$tag->id] = $tag->tag; + } + asort($list); + + $list = [0 => trans('firefly.csv_do_not_map')] + $list; + + return $list; + } +} diff --git a/app/Helpers/Csv/Mapper/TransactionCurrency.php b/app/Helpers/Csv/Mapper/TransactionCurrency.php new file mode 100644 index 0000000000..f4257b1510 --- /dev/null +++ b/app/Helpers/Csv/Mapper/TransactionCurrency.php @@ -0,0 +1,40 @@ +id] = $currency->name . ' (' . $currency->code . ')'; + } + + asort($list); + + $list = [0 => trans('firefly.csv_do_not_map')] + $list; + + return $list; + } +} diff --git a/app/Helpers/Csv/PostProcessing/Amount.php b/app/Helpers/Csv/PostProcessing/Amount.php new file mode 100644 index 0000000000..504c887331 --- /dev/null +++ b/app/Helpers/Csv/PostProcessing/Amount.php @@ -0,0 +1,44 @@ +data['amount'] ?? '0'; + $modifier = strval($this->data['amount-modifier']); + $this->data['amount'] = bcmul($amount, $modifier); + + return $this->data; + } + + /** + * @param array $data + */ + public function setData(array $data) + { + $this->data = $data; + } +} diff --git a/app/Helpers/Csv/PostProcessing/AssetAccount.php b/app/Helpers/Csv/PostProcessing/AssetAccount.php new file mode 100644 index 0000000000..e57e0f73f1 --- /dev/null +++ b/app/Helpers/Csv/PostProcessing/AssetAccount.php @@ -0,0 +1,274 @@ +checkIdNameObject(); // has object in ID or Name? + if (!is_null($result)) { + return $result; + } + + // no object? maybe asset-account-iban is a string and we can find the matching account. + $result = $this->checkIbanString(); + if (!is_null($result)) { + return $result; + } + + // no object still? maybe we can find the account by name. + $result = $this->checkNameString(); + if (!is_null($result)) { + return $result; + } + // still nothing? Perhaps the account number can lead us to an account: + $result = $this->checkAccountNumberString(); + if (!is_null($result)) { + return $result; + } + + return null; + } + + /** + * @param array $data + */ + public function setData(array $data) + { + $this->data = $data; + } + + /** + * @return array|null + */ + protected function checkAccountNumberString() + { + $accountNumber = $this->data['asset-account-number'] ?? null; + if ($accountNumber instanceof Account) { // fourth: try to find account based on name, if any. + $this->data['asset-account-object'] = $accountNumber; + + return $this->data; + } + if (is_string($accountNumber)) { // it's an actual account number + $this->data['asset-account-object'] = $this->parseAccountNumberString(); + + return $this->data; + } + + return null; + } + + /** + * @return array|null + */ + protected function checkIbanString() + { + $iban = $this->data['asset-account-iban'] ?? ''; + $rules = ['iban' => 'iban']; + $check = ['iban' => $iban]; + $validator = Validator::make($check, $rules); + if (!$validator->fails()) { + $this->data['asset-account-object'] = $this->parseIbanString(); + + return $this->data; + } + + return null; + } + + /** + * @return array + */ + protected function checkIdNameObject() + { + $accountId = $this->data['asset-account-id'] ?? null; + $accountIban = $this->data['asset-account-iban'] ?? null; + $accountNumber = $this->data['asset-account-number'] ?? null; + if ($accountId instanceof Account) { // first priority. try to find the account based on ID, if any + $this->data['asset-account-object'] = $accountId; + + return $this->data; + } + if ($accountIban instanceof Account) { // second: try to find the account based on IBAN, if any. + $this->data['asset-account-object'] = $accountIban; + + return $this->data; + } + + if ($accountNumber instanceof Account) { // second: try to find the account based on account number, if any. + $this->data['asset-account-object'] = $accountNumber; + + return $this->data; + } + + + return null; + } + + /** + * @return array|null + */ + protected function checkNameString() + { + $accountName = $this->data['asset-account-name'] ?? null; + if ($accountName instanceof Account) { // third: try to find account based on name, if any. + $this->data['asset-account-object'] = $accountName; + + return $this->data; + } + if (is_string($accountName)) { + $this->data['asset-account-object'] = $this->parseNameString(); + + return $this->data; + } + + return null; + } + + /** + * @return Account|null + */ + protected function createAccount() + { + $accountType = $this->getAccountType(); + $name = $this->data['asset-account-name'] ?? ''; + $iban = $this->data['asset-account-iban'] ?? ''; + + // create if not exists: // See issue #180 + $name = strlen($name) > 0 ? $name : $iban; + $account = Account::firstOrCreateEncrypted( + [ + 'user_id' => Auth::user()->id, + 'account_type_id' => $accountType->id, + 'name' => $name, + 'iban' => $iban, + 'active' => true, + ] + ); + + return $account; + } + + /** + * + * @return AccountType + */ + protected function getAccountType() + { + return AccountType::where('type', 'Asset account')->first(); + } + + /** + * @return Account|null + */ + protected function parseIbanString() + { + // create by name and/or iban. + $iban = $this->data['asset-account-iban'] ?? ''; + $accounts = Auth::user()->accounts()->get(); + foreach ($accounts as $entry) { + if ($iban !== '' && $entry->iban === $iban) { + + return $entry; + } + } + $account = $this->createAccount(); + + return $account; + } + + /** + * @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['asset-account-name']) { + + return $entry; + } + } + // create if not exists: + // See issue #180 + $account = Account::firstOrCreateEncrypted( + [ + 'user_id' => Auth::user()->id, + 'account_type_id' => $accountType->id, + 'name' => $this->data['asset-account-name'], + 'iban' => '', + 'active' => true, + ] + ); + + return $account; + } + + /** + * @return Account|null + */ + private function parseAccountNumberString() + { + /** @var AccountCrudInterface $crud */ + $crud = app(AccountCrudInterface::class); + + $accountNumber = $this->data['asset-account-number'] ?? ''; + $accountType = $this->getAccountType(); + $accounts = Auth::user()->accounts()->with(['accountmeta'])->where('account_type_id', $accountType->id)->get(); + /** @var Account $entry */ + foreach ($accounts as $entry) { + $metaFieldValue = $entry->getMeta('accountNumber'); + if ($metaFieldValue === $accountNumber && $metaFieldValue !== '') { + + return $entry; + } + } + // create new if not exists and return that one: + $accountData = [ + 'name' => $accountNumber, + 'accountType' => 'asset', + 'virtualBalance' => 0, + 'virtualBalanceCurrency' => 1, // hard coded. + 'active' => true, + 'user' => Auth::user()->id, + 'iban' => null, + 'accountNumber' => $accountNumber, + 'accountRole' => null, + 'openingBalance' => 0, + 'openingBalanceDate' => new Carbon, + 'openingBalanceCurrency' => 1, // hard coded. + ]; + $account = $crud->store($accountData); + + return $account; + } +} diff --git a/app/Helpers/Csv/PostProcessing/Bill.php b/app/Helpers/Csv/PostProcessing/Bill.php new file mode 100644 index 0000000000..60392b2446 --- /dev/null +++ b/app/Helpers/Csv/PostProcessing/Bill.php @@ -0,0 +1,45 @@ +data['bill']) && !is_null($this->data['bill']->id)) { + $this->data['bill-id'] = $this->data['bill']->id; + } + + return $this->data; + } + + /** + * @param array $data + */ + public function setData(array $data) + { + $this->data = $data; + } +} diff --git a/app/Helpers/Csv/PostProcessing/Currency.php b/app/Helpers/Csv/PostProcessing/Currency.php new file mode 100644 index 0000000000..2a6f46cfd5 --- /dev/null +++ b/app/Helpers/Csv/PostProcessing/Currency.php @@ -0,0 +1,49 @@ +data['currency'])) { + $currencyPreference = Preferences::get('currencyPreference', env('DEFAULT_CURRENCY', 'EUR')); + $this->data['currency'] = TransactionCurrency::whereCode($currencyPreference->data)->first(); + } + + return $this->data; + } + + /** + * @param array $data + */ + public function setData(array $data) + { + $this->data = $data; + } +} diff --git a/app/Helpers/Csv/PostProcessing/Description.php b/app/Helpers/Csv/PostProcessing/Description.php new file mode 100644 index 0000000000..8c4843a58b --- /dev/null +++ b/app/Helpers/Csv/PostProcessing/Description.php @@ -0,0 +1,47 @@ +data['description'] ?? ''; + $this->data['description'] = trim($description); + if (strlen($this->data['description']) == 0) { + $this->data['description'] = trans('firefly.csv_empty_description'); + } + + + return $this->data; + } + + /** + * @param array $data + */ + public function setData(array $data) + { + + $this->data = $data; + } +} diff --git a/app/Helpers/Csv/PostProcessing/OpposingAccount.php b/app/Helpers/Csv/PostProcessing/OpposingAccount.php new file mode 100644 index 0000000000..279328b24e --- /dev/null +++ b/app/Helpers/Csv/PostProcessing/OpposingAccount.php @@ -0,0 +1,210 @@ +checkIdNameObject(); + if (!is_null($result)) { + return $result; + } + + $result = $this->checkIbanString(); + if (!is_null($result)) { + return $result; + } + + $result = $this->checkNameString(); + if (!is_null($result)) { + return $result; + } + + return null; + } + + /** + * @param array $data + */ + public function setData(array $data) + { + $this->data = $data; + } + + /** + * @return array|null + */ + protected function checkIbanString() + { + $rules = ['iban' => 'iban']; + $iban = $this->data['opposing-account-iban']; + $check = ['iban' => $iban]; + $validator = Validator::make($check, $rules); + if (is_string($iban) && strlen($iban) > 0 && !$validator->fails()) { + + $this->data['opposing-account-object'] = $this->parseIbanString(); + + return $this->data; + } + + return null; + } + + /** + * @return array + */ + protected function checkIdNameObject() + { + if ($this->data['opposing-account-id'] instanceof Account) { // first priority. try to find the account based on ID, if any + $this->data['opposing-account-object'] = $this->data['opposing-account-id']; + + return $this->data; + } + if ($this->data['opposing-account-iban'] instanceof Account) { // second: try to find the account based on IBAN, if any. + $this->data['opposing-account-object'] = $this->data['opposing-account-iban']; + + return $this->data; + } + + return null; + } + + /** + * @return array|null + */ + protected function checkNameString() + { + if ($this->data['opposing-account-name'] instanceof Account) { // third: try to find account based on name, if any. + $this->data['opposing-account-object'] = $this->data['opposing-account-name']; + + return $this->data; + } + if (is_string($this->data['opposing-account-name'])) { + + $this->data['opposing-account-object'] = $this->parseNameString(); + + return $this->data; + } + + return null; + } + + /** + * @return Account|null + */ + protected function createAccount() + { + $accountType = $this->getAccountType(); + + // create if not exists: + $name = is_string($this->data['opposing-account-name']) && strlen($this->data['opposing-account-name']) > 0 ? $this->data['opposing-account-name'] + : $this->data['opposing-account-iban']; + $account = Account::firstOrCreateEncrypted( // See issue #180 + [ + 'user_id' => Auth::user()->id, + 'account_type_id' => $accountType->id, + 'name' => $name, + '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(); + } + + // create revenue account: + + return AccountType::where('type', 'Revenue account')->first(); + + + } + + /** + * @return Account|null + */ + protected function parseIbanString() + { + // create by name and/or iban. + $accounts = Auth::user()->accounts()->get(); + foreach ($accounts as $entry) { + if ($entry->iban == $this->data['opposing-account-iban']) { + + return $entry; + } + } + $account = $this->createAccount(); + + + return $account; + } + + /** + * @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( // See issue #180 + [ + 'user_id' => Auth::user()->id, + 'account_type_id' => $accountType->id, + 'name' => $this->data['opposing-account-name'], + 'iban' => '', + 'active' => true, + ] + ); + + return $account; + } +} diff --git a/app/Helpers/Csv/PostProcessing/PostProcessorInterface.php b/app/Helpers/Csv/PostProcessing/PostProcessorInterface.php new file mode 100644 index 0000000000..58f6ff09d0 --- /dev/null +++ b/app/Helpers/Csv/PostProcessing/PostProcessorInterface.php @@ -0,0 +1,31 @@ +setProcessorType(self::POST_PROCESSOR); + } + + + /** + * @return array + */ + public function fix(): array + { + // Try to parse the description in known formats. + $parsed = $this->parseSepaDescription() || $this->parseTRTPDescription() || $this->parseGEABEADescription() || $this->parseABNAMRODescription(); + + // If the description could not be parsed, specify an unknown opposing + // account, as an opposing account is required + if (!$parsed) { + $this->data['opposing-account-name'] = trans('firefly.unknown'); + } + + return $this->data; + } + + /** + * @param array $data + */ + public function setData(array $data) + { + $this->data = $data; + } + + /** + * @param array $row + */ + public function setRow(array $row) + { + $this->row = $row; + } + + /** + * Parses the current description with costs from ABN AMRO itself + * + * @return bool true if the description is GEA/BEA-format, false otherwise + */ + protected function parseABNAMRODescription() + { + // See if the current description is formatted in ABN AMRO format + if (preg_match('/ABN AMRO.{24} (.*)/', $this->data['description'], $matches)) { + + $this->data['opposing-account-name'] = 'ABN AMRO'; + $this->data['description'] = $matches[1]; + + return true; + } + + return false; + } + + /** + * Parses the current description in GEA/BEA format + * + * @return bool true if the description is GEA/BEAformat, false otherwise + */ + protected function parseGEABEADescription() + { + // See if the current description is formatted in GEA/BEA format + if (preg_match('/([BG]EA) +(NR:[a-zA-Z:0-9]+) +([0-9.\/]+) +([^,]*)/', $this->data['description'], $matches)) { + + // description and opposing account will be the same. + $this->data['opposing-account-name'] = $matches[4]; + $this->data['description'] = $matches[4]; + + if ($matches[1] == 'GEA') { + $this->data['description'] = 'GEA ' . $matches[4]; + } + + return true; + } + + return false; + } + + /** + * Parses the current description in SEPA format + * + * @return bool true if the description is SEPA format, false otherwise + */ + protected function parseSepaDescription() + { + // See if the current description is formatted as a SEPA plain description + if (preg_match('/^SEPA(.{28})/', $this->data['description'], $matches)) { + + $type = $matches[1]; + $reference = ''; + $name = ''; + $newDescription = ''; + + // SEPA plain descriptions contain several key-value pairs, split by a colon + preg_match_all('/([A-Za-z]+(?=:\s)):\s([A-Za-z 0-9._#-]+(?=\s|$))/', $this->data['description'], $matches, PREG_SET_ORDER); + + if (is_array($matches)) { + foreach ($matches as $match) { + $key = $match[1]; + $value = trim($match[2]); + switch (strtoupper($key)) { + case 'OMSCHRIJVING': + $newDescription = $value; + break; + case 'NAAM': + $this->data['opposing-account-name'] = $value; + $name = $value; + break; + case 'KENMERK': + $reference = $value; + break; + case 'IBAN': + $this->data['opposing-account-iban'] = $value; + break; + default: + // Ignore the rest + } + } + } + + // Set a new description for the current transaction. If none was given + // set the description to type, name and reference + $this->data['description'] = $newDescription; + if (strlen($newDescription) === 0) { + $this->data['description'] = sprintf('%s - %s (%s)', $type, $name, $reference); + } + + return true; + } + + return false; + } + + /** + * Parses the current description in TRTP format + * + * @return bool true if the description is TRTP format, false otherwise + */ + protected function parseTRTPDescription() + { + // See if the current description is formatted in TRTP format + if (preg_match_all('!\/([A-Z]{3,4})\/([^/]*)!', $this->data['description'], $matches, PREG_SET_ORDER)) { + + $type = ''; + $name = ''; + $reference = ''; + $newDescription = ''; + + // Search for properties specified in the TRTP format. If no description + // is provided, use the type, name and reference as new description + if (is_array($matches)) { + foreach ($matches as $match) { + $key = $match[1]; + $value = trim($match[2]); + + switch (strtoupper($key)) { + case 'NAME': + $this->data['opposing-account-name'] = $name = $value; + break; + case 'REMI': + $newDescription = $value; + break; + case 'IBAN': + $this->data['opposing-account-iban'] = $value; + break; + case 'EREF': + $reference = $value; + break; + case 'TRTP': + $type = $value; + break; + default: + // Ignore the rest + } + } + + // Set a new description for the current transaction. If none was given + // set the description to type, name and reference + $this->data['description'] = $newDescription; + if (strlen($newDescription) === 0) { + $this->data['description'] = sprintf('%s - %s (%s)', $type, $name, $reference); + } + } + + return true; + } + + return false; + } + +} diff --git a/app/Helpers/Csv/Specifix/Dummy.php b/app/Helpers/Csv/Specifix/Dummy.php new file mode 100644 index 0000000000..447bafadd9 --- /dev/null +++ b/app/Helpers/Csv/Specifix/Dummy.php @@ -0,0 +1,60 @@ +setProcessorType(self::POST_PROCESSOR); + } + + /** + * @return array + */ + public function fix(): array + { + return $this->data; + + } + + /** + * @param array $data + */ + public function setData(array $data) + { + $this->data = $data; + } + + /** + * @param array $row + */ + public function setRow(array $row) + { + $this->row = $row; + } + + +} diff --git a/app/Helpers/Csv/Specifix/RabobankDescription.php b/app/Helpers/Csv/Specifix/RabobankDescription.php new file mode 100644 index 0000000000..67dbdb6c49 --- /dev/null +++ b/app/Helpers/Csv/Specifix/RabobankDescription.php @@ -0,0 +1,76 @@ +setProcessorType(self::POST_PROCESSOR); + } + + + /** + * @return array + */ + public function fix(): array + { + $this->rabobankFixEmptyOpposing(); + + return $this->data; + + } + + /** + * @param array $data + */ + public function setData(array $data) + { + $this->data = $data; + } + + /** + * @param array $row + */ + public function setRow(array $row) + { + $this->row = $row; + } + + /** + * Fixes Rabobank specific thing. + */ + protected function rabobankFixEmptyOpposing() + { + if (is_string($this->data['opposing-account-name']) && strlen($this->data['opposing-account-name']) == 0) { + $this->data['opposing-account-name'] = $this->row[10]; + + $this->data['description'] = trim(str_replace($this->row[10], '', $this->data['description'])); + } + + } + + +} diff --git a/app/Helpers/Csv/Specifix/Specifix.php b/app/Helpers/Csv/Specifix/Specifix.php new file mode 100644 index 0000000000..8626d38dcb --- /dev/null +++ b/app/Helpers/Csv/Specifix/Specifix.php @@ -0,0 +1,46 @@ +processorType; + } + + /** + * @param int $processorType + * + * @return $this + */ + public function setProcessorType(int $processorType) + { + $this->processorType = $processorType; + + return $this; + } + + +} diff --git a/app/Helpers/Csv/Specifix/SpecifixInterface.php b/app/Helpers/Csv/Specifix/SpecifixInterface.php new file mode 100644 index 0000000000..e37d4d3ec4 --- /dev/null +++ b/app/Helpers/Csv/Specifix/SpecifixInterface.php @@ -0,0 +1,49 @@ + $row) { + if ($this->useRow($hasHeaders, $index)) { + // collect all map values + + foreach ($keys as $column) { + $values[$column][] = $row[$column]; + } + } + } + /* + * Make each one unique. + */ + $values = $this->uniqueRecursive($values); + + return $values; + } + + /** + * @param array $roles + * @param array $map + * + * @return array + */ + public function processSelectedMapping(array $roles, array $map): array + { + $configRoles = config('csv.roles'); + $maps = []; + $keys = array_keys($map); + foreach ($keys as $index) { + if (isset($roles[$index])) { + $name = $roles[$index]; + if ($configRoles[$name]['mappable']) { + $maps[$index] = $name; + } + } + } + + return $maps; + + } + + /** + * @param array $input + * + * @return array + */ + public function processSelectedRoles(array $input): array + { + $roles = []; + + + /* + * Store all rows for each column: + */ + if (is_array($input)) { + foreach ($input as $index => $role) { + if ($role != '_ignore') { + $roles[$index] = $role; + } + } + } + + return $roles; + } + + /** + * @param array $fields + * + * @return bool + */ + public function sessionHasValues(array $fields): bool + { + foreach ($fields as $field) { + if (!Session::has($field)) { + Log::error('Session is missing field: ' . $field); + + return false; + } + } + + return true; + } + + /** + * @param array $map + * + * @return array + * @throws FireflyException + */ + public function showOptions(array $map): array + { + $options = []; + foreach ($map as $index => $columnRole) { + + $mapper = config('csv.roles.' . $columnRole . '.mapper'); + if (is_null($mapper)) { + throw new FireflyException('Cannot map field of type "' . $columnRole . '".'); + } + $class = 'FireflyIII\Helpers\Csv\Mapper\\' . $mapper; + try { + /** @var MapperInterface $mapObject */ + $mapObject = app($class); + } catch (ReflectionException $e) { + throw new FireflyException('Column "' . $columnRole . '" cannot be mapped because mapper class ' . $mapper . ' does not exist.'); + } + $set = $mapObject->getMap(); + $options[$index] = $set; + } + + return $options; + } + + /** + * @param string $path + * + * @return string + */ + public function storeCsvFile(string $path): string + { + $time = str_replace(' ', '-', microtime()); + $fileName = 'csv-upload-' . Auth::user()->id . '-' . $time . '.csv.encrypted'; + $disk = Storage::disk('upload'); + $file = new SplFileObject($path, 'r'); + $content = $file->fread($file->getSize()); + $contentEncrypted = Crypt::encrypt($content); + $disk->put($fileName, $contentEncrypted); + + return $fileName; + + + } + + /** + * @param array $array + * + * @return array + */ + protected function uniqueRecursive(array $array) + { + foreach ($array as $column => $found) { + $array[$column] = array_unique($found); + } + + return $array; + } + + /** + * @param bool $hasHeaders + * @param int $index + * + * @return bool + */ + protected function useRow(bool $hasHeaders, int $index) + { + return ($hasHeaders && $index > 1) || !$hasHeaders; + } +} diff --git a/app/Helpers/Csv/WizardInterface.php b/app/Helpers/Csv/WizardInterface.php new file mode 100644 index 0000000000..cedcfbe5f6 --- /dev/null +++ b/app/Helpers/Csv/WizardInterface.php @@ -0,0 +1,68 @@ +