From a905cce2c9c718ceaf8d489e895e5d0f66c95690 Mon Sep 17 00:00:00 2001 From: James Cole Date: Tue, 20 Jun 2017 21:04:25 +0200 Subject: [PATCH] Expand import routine. --- app/Console/Commands/CreateImport.php | 2 +- app/Console/Commands/Import.php | 15 +- .../Controllers/TransactionController.php | 4 +- app/Import/FileProcessor/CsvProcessor.php | 13 +- app/Import/Object/ImportAccount.php | 219 ++++++++++++------ app/Import/Object/ImportBudget.php | 189 +++++++++++++++ app/Import/Object/ImportCategory.php | 186 ++++++++++++++- app/Import/Object/ImportCurrency.php | 8 +- app/Import/Object/ImportJournal.php | 87 +++++-- app/Import/Routine/ImportRoutine.php | 1 + app/Import/Storage/ImportStorage.php | 114 ++++++++- .../Account/AccountRepository.php | 7 +- .../Account/FindAccountsTrait.php | 4 +- app/Support/Amount.php | 17 +- app/Support/Steam.php | 12 + database/seeds/TransactionCurrencySeeder.php | 1 + 16 files changed, 762 insertions(+), 117 deletions(-) diff --git a/app/Console/Commands/CreateImport.php b/app/Console/Commands/CreateImport.php index 3c0d14fb18..15ed5cd468 100644 --- a/app/Console/Commands/CreateImport.php +++ b/app/Console/Commands/CreateImport.php @@ -88,7 +88,7 @@ class CreateImport extends Command $this->line('Stored import data...'); $job->configuration = $configurationData; - $job->status = 'settings_complete'; + $job->status = 'configured'; $job->save(); $this->line('Stored configuration...'); diff --git a/app/Console/Commands/Import.php b/app/Console/Commands/Import.php index d2ff9b8997..6b54233707 100644 --- a/app/Console/Commands/Import.php +++ b/app/Console/Commands/Import.php @@ -13,8 +13,8 @@ declare(strict_types=1); namespace FireflyIII\Console\Commands; -use FireflyIII\Import\ImportProcedure; use FireflyIII\Import\Logging\CommandHandler; +use FireflyIII\Import\Routine\ImportRoutine; use FireflyIII\Models\ImportJob; use FireflyIII\Models\TransactionJournal; use Illuminate\Console\Command; @@ -75,15 +75,16 @@ class Import extends Command $monolog = Log::getMonolog(); $handler = new CommandHandler($this); $monolog->pushHandler($handler); - $importProcedure = new ImportProcedure; - $result = $importProcedure->runImport($job); + + $routine = new ImportRoutine($job); + $routine->run(); // display result to user: - $this->presentResults($result); + //$this->presentResults($result); $this->line('The import has completed.'); // get any errors from the importer: - $this->presentErrors($job); + //$this->presentErrors($job); return; } @@ -96,12 +97,14 @@ class Import extends Command private function isValid(ImportJob $job): bool { if (is_null($job)) { + Log::error('This job does not seem to exist.'); $this->error('This job does not seem to exist.'); return false; } - if ($job->status != 'settings_complete') { + if ($job->status !== 'configured') { + Log::error(sprintf('This job is not ready to be imported (status is %s).', $job->status)); $this->error('This job is not ready to be imported.'); return false; diff --git a/app/Http/Controllers/TransactionController.php b/app/Http/Controllers/TransactionController.php index 2ac6a55db8..764de4c669 100644 --- a/app/Http/Controllers/TransactionController.php +++ b/app/Http/Controllers/TransactionController.php @@ -78,6 +78,7 @@ class TransactionController extends Controller $start = null; $end = null; $periods = new Collection; + $path = '/transactions/' . $what; // prep for "all" view. if ($moment === 'all') { @@ -85,6 +86,7 @@ class TransactionController extends Controller $first = $repository->first(); $start = $first->date ?? new Carbon; $end = new Carbon; + $path = '/transactions/'.$what.'/all/'; } // prep for "specific date" view. @@ -118,7 +120,7 @@ class TransactionController extends Controller $collector->setAllAssetAccounts()->setRange($start, $end)->setTypes($types)->setLimit($pageSize)->setPage($page)->withOpposingAccount(); $collector->removeFilter(InternalTransferFilter::class); $journals = $collector->getPaginatedJournals(); - $journals->setPath('/transactions/' . $what); + $journals->setPath($path); $count = $journals->getCollection()->count(); if ($count === 0) { $start->subDay(); diff --git a/app/Import/FileProcessor/CsvProcessor.php b/app/Import/FileProcessor/CsvProcessor.php index 64bda5b06c..9120ff2e34 100644 --- a/app/Import/FileProcessor/CsvProcessor.php +++ b/app/Import/FileProcessor/CsvProcessor.php @@ -12,7 +12,6 @@ declare(strict_types=1); namespace FireflyIII\Import\FileProcessor; use FireflyIII\Exceptions\FireflyException; -use FireflyIII\Import\Converter\ConverterInterface; use FireflyIII\Import\Object\ImportJournal; use FireflyIII\Import\Specifics\SpecificInterface; use FireflyIII\Models\ImportJob; @@ -184,6 +183,7 @@ class CsvProcessor implements FileProcessorInterface /** * Take a row, build import journal by annotating each value and storing it in the import journal. + * * @param int $index * @param array $row * @@ -192,15 +192,18 @@ class CsvProcessor implements FileProcessorInterface private function importRow(int $index, array $row): ImportJournal { Log::debug(sprintf('Now at row %d', $index)); - $row = $this->specifics($row); + $row = $this->specifics($row); $journal = new ImportJournal; $journal->setUser($this->job->user); $journal->setHash(hash('sha256', json_encode($row))); foreach ($row as $rowIndex => $value) { - $annotated = $this->annotateValue($rowIndex, $value); - Log::debug('Annotated value', $annotated); - $journal->setValue($annotated); + $value = trim($value); + if (strlen($value) > 0) { + $annotated = $this->annotateValue($rowIndex, $value); + Log::debug('Annotated value', $annotated); + $journal->setValue($annotated); + } } Log::debug('ImportJournal complete, returning.'); diff --git a/app/Import/Object/ImportAccount.php b/app/Import/Object/ImportAccount.php index b2ac54c1b9..65d0460bab 100644 --- a/app/Import/Object/ImportAccount.php +++ b/app/Import/Object/ImportAccount.php @@ -16,6 +16,7 @@ use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; use FireflyIII\Repositories\Account\AccountRepositoryInterface; use FireflyIII\User; +use Illuminate\Support\Collection; use Log; /** @@ -36,6 +37,8 @@ class ImportAccount private $accountName = []; /** @var array */ private $accountNumber = []; + /** @var string */ + private $expectedType = ''; /** @var AccountRepositoryInterface */ private $repository; /** @var User */ @@ -46,77 +49,10 @@ class ImportAccount */ public function __construct() { - $this->account = new Account; - $this->repository = app(AccountRepositoryInterface::class); + $this->expectedType = AccountType::ASSET; + $this->account = new Account; + $this->repository = app(AccountRepositoryInterface::class); Log::debug('Created ImportAccount.'); - - } - - /** - * @return bool - */ - public function convertToExpense(): bool - { - if ($this->getAccount()->accountType->type === AccountType::EXPENSE) { - return true; - } - // maybe that an account of expense account type already exists? - $expenseType = AccountType::whereType(AccountType::EXPENSE)->first(); - $this->account->account_type_id = $expenseType->id; - $this->account->save(); - - return true; - } - - public function convertToRevenue(): bool - { - if ($this->getAccount()->accountType->type === AccountType::REVENUE) { - return true; - } - // maybe that an account of revenue account type already exists? - $revenueType = AccountType::whereType(AccountType::REVENUE)->first(); - $this->account->account_type_id = $revenueType->id; - $this->account->save(); - - return true; - } - - /** - * @return Account - */ - public function createAccount(): Account - { - if (!is_null($this->account->id)) { - return $this->account; - } - Log::debug('In createAccount()'); - // check if any of them is mapped: - $mapped = $this->findMappedObject(); - - if (is_null($mapped->id)) { - // none are, create new object! - $data = [ - 'accountType' => 'import', - 'name' => $this->accountName['value'] ?? '(no name)', - 'iban' => $this->accountIban['value'] ?? null, - 'active' => true, - 'virtualBalance' => null, - ]; - if (!is_null($data['iban']) && $data['name'] === '(no name)') { - $data['name'] = $data['iban']; - } - Log::debug('Search for maps resulted in nothing, create new one based on', $data); - $account = $this->repository->store($data); - $this->account = $account; - Log::info('Made new account.', ['input' => $data, 'new' => $account->toArray()]); - - - return $account; - } - Log::debug('Mapped existing account.', ['new' => $mapped->toArray()]); - $this->account = $mapped; - - return $mapped; } /** @@ -124,6 +60,10 @@ class ImportAccount */ public function getAccount(): Account { + if (is_null($this->account->id)) { + $this->store(); + } + return $this->account; } @@ -159,6 +99,14 @@ class ImportAccount $this->accountNumber = $accountNumber; } + /** + * @param string $expectedType + */ + public function setExpectedType(string $expectedType) + { + $this->expectedType = $expectedType; + } + /** * @param User $user */ @@ -168,12 +116,88 @@ class ImportAccount $this->repository->setUser($user); } + /** + * @return Account + */ + private function findExistingObject(): Account + { + Log::debug('In findExistingObject() for Account'); + // 0: determin account type: + /** @var AccountType $accountType */ + $accountType = AccountType::whereType($this->expectedType)->first(); + + // 1: find by ID, iban or name (and type) + 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(); + if (!is_null($account)) { + Log::debug(sprintf('Found unmapped %s account by ID (#%d): %s', $this->expectedType, $account->id, $account->name)); + + return $account; + } + Log::debug('Found nothing.'); + } + /** @var Collection $accounts */ + $accounts = $this->repository->getAccountsByType([$accountType->type]); + // 2: find by IBAN (and type): + if (count($this->accountIban) === 3) { + $iban = $this->accountIban['value']; + 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) { + Log::debug( + sprintf('Found unmapped %s account by IBAN (#%d): %s (%s)', $this->expectedType, $account->id, $account->name, $account->iban) + ); + + return $account; + } + + return null; + } + ); + if ($filtered->count() === 1) { + return $filtered->first(); + } + Log::debug('Found nothing.'); + } + + // 3: find by name (and type): + if (count($this->accountName) === 3) { + $name = $this->accountName['value']; + 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) { + Log::debug(sprintf('Found unmapped %s account by name (#%d): %s', $this->expectedType, $account->id, $account->name)); + + return $account; + } + + return null; + } + ); + + if ($filtered->count() === 1) { + return $filtered->first(); + } + Log::debug('Found nothing.'); + } + + // 4: do not search by account number. + Log::debug('Found NO existing accounts.'); + + return new Account; + + } + /** * @return Account */ private function findMappedObject(): Account { - Log::debug('In findMappedObject()'); + Log::debug('In findMappedObject() for Account'); $fields = ['accountId', 'accountIban', 'accountNumber', 'accountName']; foreach ($fields as $field) { $array = $this->$field; @@ -199,7 +223,7 @@ class ImportAccount */ private function getMappedObject(array $array): Account { - Log::debug('In getMappedObject()'); + Log::debug('In getMappedObject() for Account'); if (count($array) === 0) { Log::debug('Array is empty, nothing will come of this.'); @@ -212,7 +236,7 @@ class ImportAccount return new Account; } - Log::debug('Finding a mapped object based on', $array); + Log::debug('Finding a mapped account based on', $array); $search = intval($array['mapped']); $account = $this->repository->find($search); @@ -222,5 +246,54 @@ class ImportAccount return $account; } + /** + * @return bool + */ + private function store(): bool + { + // 1: find mapped object: + $mapped = $this->findMappedObject(); + if (!is_null($mapped->id)) { + $this->account = $mapped; + + return true; + } + // 2: find existing by given values: + $found = $this->findExistingObject(); + if (!is_null($found->id)) { + $this->account = $found; + + return true; + } + + // 3: if found nothing, retry the search with an asset account: + Log::debug('Will try to find an asset account just in case.'); + $oldExpectedType = $this->expectedType; + $this->expectedType = AccountType::ASSET; + $found = $this->findExistingObject(); + if (!is_null($found->id)) { + Log::debug('Found asset account!'); + $this->account = $found; + + return true; + } + $this->expectedType = $oldExpectedType; + + Log::debug(sprintf('Found no account of type %s so must create one ourselves.', $this->expectedType)); + + $data = [ + 'accountType' => config('firefly.shortNamesByFullName.' . $this->expectedType), + 'name' => $this->accountName['value'] ?? '(no name)', + 'iban' => $this->accountIban['value'] ?? null, + 'active' => true, + 'virtualBalance' => null, + ]; + + $this->account = $this->repository->store($data); + Log::debug(sprintf('Successfully stored new account #%d: %s', $this->account->id, $this->account->name)); + + return true; + } + } \ No newline at end of file diff --git a/app/Import/Object/ImportBudget.php b/app/Import/Object/ImportBudget.php index bebfda12cf..b3732a4efa 100644 --- a/app/Import/Object/ImportBudget.php +++ b/app/Import/Object/ImportBudget.php @@ -12,13 +12,52 @@ declare(strict_types=1); namespace FireflyIII\Import\Object; +use FireflyIII\Models\Budget; +use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; +use FireflyIII\User; +use Illuminate\Support\Collection; +use Log; + +/** + * Class ImportBudget + * + * @package FireflyIII\Import\Object + */ class ImportBudget { + /** @var Budget */ + private $budget; /** @var array */ private $id = []; /** @var array */ private $name = []; + /** @var BudgetRepositoryInterface */ + private $repository; + /** @var User */ + private $user; + + /** + * ImportBudget constructor. + */ + public function __construct() + { + $this->budget = new Budget; + $this->repository = app(BudgetRepositoryInterface::class); + Log::debug('Created ImportBudget.'); + } + + /** + * @return Budget + */ + public function getBudget(): Budget + { + if (is_null($this->budget->id)) { + $this->store(); + } + + return $this->budget; + } /** * @param array $id @@ -36,5 +75,155 @@ class ImportBudget $this->name = $name; } + /** + * @param User $user + */ + public function setUser(User $user) + { + $this->user = $user; + $this->repository->setUser($user); + } + + /** + * @return Budget + */ + private function findExistingObject(): Budget + { + Log::debug('In findExistingObject() for Budget'); + // 1: find by ID, or name + + if (count($this->id) === 3) { + Log::debug(sprintf('Finding budget with ID #%d', $this->id['value'])); + /** @var Budget $budget */ + $budget = $this->repository->find(intval($this->id['value'])); + if (!is_null($budget->id)) { + Log::debug(sprintf('Found unmapped budget by ID (#%d): %s', $budget->id, $budget->name)); + + return $budget; + } + Log::debug('Found nothing.'); + } + // 2: find by name + if (count($this->name) === 3) { + /** @var Collection $budgets */ + $budgets = $this->repository->getBudgets(); + $name = $this->name['value']; + Log::debug(sprintf('Finding budget with name %s', $name)); + $filtered = $budgets->filter( + function (Budget $budget) use ($name) { + if ($budget->name === $name) { + Log::debug(sprintf('Found unmapped budget by name (#%d): %s', $budget->id, $budget->name)); + + return $budget; + } + + return null; + } + ); + + if ($filtered->count() === 1) { + return $filtered->first(); + } + Log::debug('Found nothing.'); + } + + // 4: do not search by account number. + Log::debug('Found NO existing budgets.'); + + return new Budget; + + } + + /** + * @return Budget + */ + private function findMappedObject(): Budget + { + Log::debug('In findMappedObject() for Budget'); + $fields = ['id', 'name']; + foreach ($fields as $field) { + $array = $this->$field; + Log::debug(sprintf('Find mapped budget based on field "%s" with value', $field), $array); + // check if a pre-mapped object exists. + $mapped = $this->getMappedObject($array); + if (!is_null($mapped->id)) { + Log::debug(sprintf('Found budget #%d!', $mapped->id)); + + return $mapped; + } + + } + Log::debug('Found no budget on mapped data or no map present.'); + + return new Budget; + } + + /** + * @param array $array + * + * @return Budget + */ + private function getMappedObject(array $array): Budget + { + Log::debug('In getMappedObject() for Budget'); + if (count($array) === 0) { + Log::debug('Array is empty, nothing will come of this.'); + + return new Budget; + } + + if (array_key_exists('mapped', $array) && is_null($array['mapped'])) { + Log::debug(sprintf('No map present for value "%s". Return NULL.', $array['value'])); + + return new Budget; + } + + Log::debug('Finding a mapped budget based on', $array); + + $search = intval($array['mapped']); + $account = $this->repository->find($search); + + Log::debug(sprintf('Found budget! #%d ("%s"). Return it', $account->id, $account->name)); + + return $account; + } + + /** + * @return bool + */ + private function store(): bool + { + // 1: find mapped object: + $mapped = $this->findMappedObject(); + if (!is_null($mapped->id)) { + $this->budget = $mapped; + + return true; + } + // 2: find existing by given values: + $found = $this->findExistingObject(); + if (!is_null($found->id)) { + $this->budget = $found; + + return true; + } + $name = $this->name['value'] ?? ''; + + if (strlen($name) === 0) { + return true; + } + + Log::debug('Found no budget so must create one ourselves.'); + + $data = [ + 'name' => $name, + ]; + + $this->budget = $this->repository->store($data); + Log::debug(sprintf('Successfully stored new budget #%d: %s', $this->budget->id, $this->budget->name)); + + return true; + } + } \ No newline at end of file diff --git a/app/Import/Object/ImportCategory.php b/app/Import/Object/ImportCategory.php index c59b457724..f8ee78442e 100644 --- a/app/Import/Object/ImportCategory.php +++ b/app/Import/Object/ImportCategory.php @@ -12,14 +12,24 @@ declare(strict_types=1); namespace FireflyIII\Import\Object; +use FireflyIII\Models\Category; +use FireflyIII\Repositories\Category\CategoryRepositoryInterface; +use FireflyIII\User; +use Illuminate\Support\Collection; +use Log; + class ImportCategory { - + /** @var Category */ + private $category; /** @var array */ private $id = []; /** @var array */ private $name = []; - + /** @var CategoryRepositoryInterface */ + private $repository; + /** @var User */ + private $user; /** * @param array $id */ @@ -28,6 +38,16 @@ class ImportCategory $this->id = $id; } + /** + * ImportCategory constructor. + */ + public function __construct() + { + $this->category = new Category(); + $this->repository = app(CategoryRepositoryInterface::class); + Log::debug('Created ImportCategory.'); + } + /** * @param array $name */ @@ -37,4 +57,166 @@ class ImportCategory } + /** + * @return Category + */ + public function getCategory(): Category + { + if (is_null($this->category->id)) { + $this->store(); + } + + return $this->category; + } + /** + * @param User $user + */ + public function setUser(User $user) + { + $this->user = $user; + $this->repository->setUser($user); + } + + /** + * @return Category + */ + private function findExistingObject(): Category + { + Log::debug('In findExistingObject() for Category'); + // 1: find by ID, or name + + if (count($this->id) === 3) { + Log::debug(sprintf('Finding category with ID #%d', $this->id['value'])); + /** @var Category $category */ + $category = $this->repository->find(intval($this->id['value'])); + if (!is_null($category->id)) { + Log::debug(sprintf('Found unmapped category by ID (#%d): %s', $category->id, $category->name)); + + return $category; + } + Log::debug('Found nothing.'); + } + // 2: find by name + if (count($this->name) === 3) { + /** @var Collection $categories */ + $categories = $this->repository->getCategories(); + $name = $this->name['value']; + Log::debug(sprintf('Finding category with name %s', $name)); + $filtered = $categories->filter( + function (Category $category) use ($name) { + if ($category->name === $name) { + Log::debug(sprintf('Found unmapped category by name (#%d): %s', $category->id, $category->name)); + + return $category; + } + + return null; + } + ); + + if ($filtered->count() === 1) { + return $filtered->first(); + } + Log::debug('Found nothing.'); + } + + // 4: do not search by account number. + Log::debug('Found NO existing categories.'); + + return new Category; + + } + + /** + * @return Category + */ + private function findMappedObject(): Category + { + Log::debug('In findMappedObject() for Category'); + $fields = ['id', 'name']; + foreach ($fields as $field) { + $array = $this->$field; + Log::debug(sprintf('Find mapped category based on field "%s" with value', $field), $array); + // check if a pre-mapped object exists. + $mapped = $this->getMappedObject($array); + if (!is_null($mapped->id)) { + Log::debug(sprintf('Found category #%d!', $mapped->id)); + + return $mapped; + } + + } + Log::debug('Found no category on mapped data or no map present.'); + + return new Category; + } + + /** + * @param array $array + * + * @return Category + */ + private function getMappedObject(array $array): Category + { + Log::debug('In getMappedObject() for Category'); + if (count($array) === 0) { + Log::debug('Array is empty, nothing will come of this.'); + + return new Category; + } + + if (array_key_exists('mapped', $array) && is_null($array['mapped'])) { + Log::debug(sprintf('No map present for value "%s". Return NULL.', $array['value'])); + + return new Category; + } + + Log::debug('Finding a mapped category based on', $array); + + $search = intval($array['mapped']); + $account = $this->repository->find($search); + + Log::debug(sprintf('Found category! #%d ("%s"). Return it', $account->id, $account->name)); + + return $account; + } + + /** + * @return bool + */ + private function store(): bool + { + // 1: find mapped object: + $mapped = $this->findMappedObject(); + if (!is_null($mapped->id)) { + $this->category = $mapped; + + return true; + } + // 2: find existing by given values: + $found = $this->findExistingObject(); + if (!is_null($found->id)) { + $this->category = $found; + + return true; + } + $name = $this->name['value'] ?? ''; + + if (strlen($name) === 0) { + return true; + } + + Log::debug('Found no category so must create one ourselves.'); + + $data = [ + 'name' => $name, + ]; + + $this->category = $this->repository->store($data); + Log::debug(sprintf('Successfully stored new category #%d: %s', $this->category->id, $this->category->name)); + + return true; + } + + } \ No newline at end of file diff --git a/app/Import/Object/ImportCurrency.php b/app/Import/Object/ImportCurrency.php index 4bb53c0c4d..b982e9c6b5 100644 --- a/app/Import/Object/ImportCurrency.php +++ b/app/Import/Object/ImportCurrency.php @@ -46,7 +46,7 @@ class ImportCurrency /** * @return TransactionCurrency */ - public function createCurrency(): TransactionCurrency + public function getTransactionCurrency(): TransactionCurrency { if (!is_null($this->currency->id)) { return $this->currency; @@ -77,6 +77,12 @@ class ImportCurrency 'name' => $this->name['value'] ?? null, 'decimal_places' => 2, ]; + if (is_null($data['code'])) { + Log::info('Need at least a code to create currency, return nothing.'); + + return new TransactionCurrency(); + } + Log::debug('Search for maps resulted in nothing, create new one based on', $data); $currency = $this->repository->store($data); $this->currency = $currency; diff --git a/app/Import/Object/ImportJournal.php b/app/Import/Object/ImportJournal.php index e83921f0ee..859bd5a2c0 100644 --- a/app/Import/Object/ImportJournal.php +++ b/app/Import/Object/ImportJournal.php @@ -14,11 +14,12 @@ namespace FireflyIII\Import\Object; use Carbon\Carbon; use FireflyIII\Exceptions\FireflyException; -use FireflyIII\Models\AccountType; +use FireflyIII\Import\Converter\Amount; +use FireflyIII\Import\Converter\ConverterInterface; use FireflyIII\Models\TransactionJournal; -use FireflyIII\Models\TransactionType; use FireflyIII\User; use Illuminate\Support\Collection; +use Steam; /** * Class ImportJournal @@ -27,35 +28,33 @@ use Illuminate\Support\Collection; */ class ImportJournal { - /** @var Collection */ - public $errors; - /** @var string */ - private $amount = '0'; /** @var ImportAccount */ public $asset; + /** @var ImportBudget */ + public $budget; + /** @var string */ + public $description; + /** @var Collection */ + public $errors; + /** @var string */ + public $hash; + /** @var ImportAccount */ + public $opposing; + /** @var string */ + private $amount = '0'; /** @var ImportBill */ private $bill; - /** @var ImportBudget */ - private $budget; /** @var ImportCategory */ - private $category; + public $category; /** @var ImportCurrency */ private $currency; /** @var string */ private $date = ''; /** @var string */ - private $dateFormat = 'Ymd'; - /** @var string */ - private $description; - /** @var string */ private $externalId = ''; - /** @var string */ - private $hash; /** @var array */ private $modifiers = []; - /** @var ImportAccount */ - private $opposing; - private $tags = []; + private $tags = []; /** @var string */ private $transactionType = ''; /** @var User */ @@ -72,6 +71,7 @@ class ImportJournal $this->bill = new ImportBill; $this->category = new ImportCategory; $this->budget = new ImportBudget; + $this->currency = new ImportCurrency; } /** @@ -91,15 +91,66 @@ class ImportJournal exit('does not work yet'); } + /** + * @return string + */ + public function getAmount(): string + { + + /** @var ConverterInterface $amountConverter */ + $amountConverter = app(Amount::class); + $this->amount = strval($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 + * + * @return Carbon + */ + public function getDate(string $format): Carbon + { + return Carbon::createFromFormat($format, $this->date); + } + + /** + * @param string $hash + */ + public function setHash(string $hash) + { + $this->hash = $hash; + } + /** * @param User $user */ public function setUser(User $user) { $this->user = $user; + // set user for related objects: $this->asset->setUser($user); $this->opposing->setUser($user); + $this->budget->setUser($user); + $this->category->setUser($user); } /** diff --git a/app/Import/Routine/ImportRoutine.php b/app/Import/Routine/ImportRoutine.php index b74eb338bf..f881d440a3 100644 --- a/app/Import/Routine/ImportRoutine.php +++ b/app/Import/Routine/ImportRoutine.php @@ -60,6 +60,7 @@ class ImportRoutine } $storage = new ImportStorage; + $storage->setJob($this->job); $storage->setDateFormat($this->job->configuration['date-format']); $storage->setObjects($objects); $storage->store(); diff --git a/app/Import/Storage/ImportStorage.php b/app/Import/Storage/ImportStorage.php index 6223c33643..3739afbd96 100644 --- a/app/Import/Storage/ImportStorage.php +++ b/app/Import/Storage/ImportStorage.php @@ -11,11 +11,18 @@ declare(strict_types=1); namespace FireflyIII\Import\Storage; +use Amount; +use FireflyIII\Exceptions\FireflyException; use FireflyIII\Import\Object\ImportJournal; -use FireflyIII\Import\Object\ImportObject; -use FireflyIII\Models\TransactionJournalMeta; +use FireflyIII\Models\Account; +use FireflyIII\Models\AccountType; +use FireflyIII\Models\ImportJob; +use FireflyIII\Models\Transaction; +use FireflyIII\Models\TransactionJournal; +use FireflyIII\Models\TransactionType; use Illuminate\Support\Collection; use Log; +use Steam; /** * Is capable of storing individual ImportJournal objects. @@ -27,6 +34,9 @@ class ImportStorage { /** @var string */ private $dateFormat = 'Ymd'; + private $defaultCurrency; + /** @var ImportJob */ + private $job; /** @var Collection */ private $objects; @@ -46,6 +56,13 @@ class ImportStorage $this->dateFormat = $dateFormat; } + /** + * @param ImportJob $job + */ + public function setJob(ImportJob $job) + { + $this->job = $job; + } /** * @param Collection $objects @@ -61,15 +78,102 @@ class ImportStorage */ public function store() { + $this->defaultCurrency = Amount::getDefaultCurrencyByUser($this->job->user); + /** - * @var int $index + * @var int $index * @var ImportJournal $object */ foreach ($this->objects as $index => $object) { - Log::debug(sprintf('Going to store object #%d', $index)); + Log::debug(sprintf('Going to store object #%d with description "%s"', $index, $object->description)); + + // create the asset account + $asset = $object->asset->getAccount(); + $opposing = new Account; + $amount = $object->getAmount(); + $currency = $object->getCurrency()->getTransactionCurrency(); + $date = $object->getDate($this->dateFormat); + $transactionType = new TransactionType; + + if (is_null($currency->id)) { + $currency = $this->defaultCurrency; + } + + if (bccomp($amount, '0') === -1) { + // amount is negative, it's a withdrawal, opposing is an expense: + Log::debug(sprintf('%s is negative, create opposing expense account.', $amount)); + $object->opposing->setExpectedType(AccountType::EXPENSE); + $opposing = $object->opposing->getAccount(); + $transactionType = TransactionType::whereType(TransactionType::WITHDRAWAL)->first(); + } + if (bccomp($amount, '0') === 1) { + Log::debug(sprintf('%s is positive, create opposing revenue account.', $amount)); + // amount is positive, it's a deposit, opposing is an revenue: + $object->opposing->setExpectedType(AccountType::REVENUE); + $opposing = $object->opposing->getAccount(); + $transactionType = TransactionType::whereType(TransactionType::DEPOSIT)->first(); + } + + // 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(); + } + + $journal = new TransactionJournal; + $journal->user_id = $this->job->user_id; + $journal->transaction_type_id = $transactionType->id; + $journal->transaction_currency_id = $currency->id; + $journal->description = $object->description; + $journal->date = $date->format('Y-m-d'); + $journal->order = 0; + $journal->tag_count = 0; + $journal->encrypted = 0; + $journal->completed = 0; + if (!$journal->save()) { + throw new FireflyException($journal->getErrors()->first()); + } + $journal->setMeta('importHash', $object->hash); + Log::debug(sprintf('Created journal with ID #%d', $journal->id)); + + // create transactions: + $one = new Transaction; + $one->account_id = $asset->id; + $one->transaction_journal_id = $journal->id; + $one->transaction_currency_id = $currency->id; + $one->amount = $amount; + $one->save(); + Log::debug(sprintf('Created transaction with ID #%d and account #%d', $one->id, $asset->id)); + + $two = new Transaction; + $two->account_id = $opposing->id; + $two->transaction_journal_id = $journal->id; + $two->transaction_currency_id = $currency->id; + $two->amount = Steam::opposite($amount); + $two->save(); + Log::debug(sprintf('Created transaction with ID #%d and account #%d', $two->id, $opposing->id)); + + // category + $category = $object->category->getCategory(); + if (!is_null($category->id)) { + Log::debug(sprintf('Linked category #%d to journal #%d', $category->id, $journal->id)); + $journal->categories()->save($category); + } + + // budget + $budget = $object->budget->getBudget(); + if (!is_null($budget->id)) { + Log::debug(sprintf('Linked budget #%d to journal #%d', $budget->id, $journal->id)); + $journal->budgets()->save($budget); + } + // bill + + // + - die('Cannot actually store yet.'); } + + die('Cannot actually store yet.'); } } \ No newline at end of file diff --git a/app/Repositories/Account/AccountRepository.php b/app/Repositories/Account/AccountRepository.php index c7d6422a50..86bd353c0a 100644 --- a/app/Repositories/Account/AccountRepository.php +++ b/app/Repositories/Account/AccountRepository.php @@ -243,9 +243,11 @@ class AccountRepository implements AccountRepositoryInterface } // account may exist already: - $existingAccount = $this->findByName($data['name'], [$data['accountType']]); + $existingAccount = $this->findByName($data['name'], [$type]); if (!is_null($existingAccount->id)) { - throw new FireflyException(sprintf('There already is an account named "%s" of type "%s".', $data['name'], $data['accountType'])); + Log::warning(sprintf('There already is an account named "%s" of type "%s".', $data['name'], $type)); + + return $existingAccount; } // create it: @@ -267,6 +269,7 @@ class AccountRepository implements AccountRepositoryInterface ); throw new FireflyException(sprintf('Tried to create account named "%s" but failed. The logs have more details.', $data['name'])); } + Log::debug(sprintf('Created new account #%d named "%s" of type %s.', $newAccount->id, $newAccount->name, $accountType->type)); return $newAccount; } diff --git a/app/Repositories/Account/FindAccountsTrait.php b/app/Repositories/Account/FindAccountsTrait.php index 062c7c1b8f..c5186187cc 100644 --- a/app/Repositories/Account/FindAccountsTrait.php +++ b/app/Repositories/Account/FindAccountsTrait.php @@ -107,7 +107,7 @@ trait FindAccountsTrait $query->whereIn('account_types.type', $types); } - Log::debug(sprintf('Searching for account named %s of the following type(s)', $name), ['types' => $types]); + Log::debug(sprintf('Searching for account named "%s" (of user #%d) of the following type(s)', $name, $this->user->id), ['types' => $types]); $accounts = $query->get(['accounts.*']); /** @var Account $account */ @@ -118,7 +118,7 @@ trait FindAccountsTrait return $account; } } - Log::debug('Found nothing.'); + Log::debug(sprintf('There is no account with name "%s" or types', $name), $types); return new Account; } diff --git a/app/Support/Amount.php b/app/Support/Amount.php index 89ef0dea05..6d68b4b455 100644 --- a/app/Support/Amount.php +++ b/app/Support/Amount.php @@ -18,6 +18,7 @@ use FireflyIII\Models\Transaction as TransactionModel; use FireflyIII\Models\TransactionCurrency; use FireflyIII\Models\TransactionJournal; use FireflyIII\Models\TransactionType; +use FireflyIII\User; use Illuminate\Support\Collection; use Preferences as Prefs; @@ -208,13 +209,27 @@ class Amount * @throws FireflyException */ public function getDefaultCurrency(): TransactionCurrency + { + $user = auth()->user(); + + return $this->getDefaultCurrencyByUser($user); + } + + /** + * @param User $user + * + * @return \FireflyIII\Models\TransactionCurrency + * @throws FireflyException + */ + public function getDefaultCurrencyByUser(User $user): TransactionCurrency { $cache = new CacheProperties; $cache->addProperty('getDefaultCurrency'); + $cache->addProperty($user->id); if ($cache->has()) { return $cache->get(); // @codeCoverageIgnore } - $currencyPreference = Prefs::get('currencyPreference', config('firefly.default_currency', 'EUR')); + $currencyPreference = Prefs::getForUser($user, 'currencyPreference', config('firefly.default_currency', 'EUR')); $currency = TransactionCurrency::where('code', $currencyPreference->data)->first(); if (is_null($currency)) { throw new FireflyException(sprintf('No currency found with code "%s"', $currencyPreference->data)); diff --git a/app/Support/Steam.php b/app/Support/Steam.php index 6baac5685a..1acca74857 100644 --- a/app/Support/Steam.php +++ b/app/Support/Steam.php @@ -284,6 +284,18 @@ class Steam return $amount; } + /** + * @param string $amount + * + * @return string + */ + public function opposite(string $amount): string + { + $amount = bcmul($amount, '-1'); + + return $amount; + } + /** * @param $string * diff --git a/database/seeds/TransactionCurrencySeeder.php b/database/seeds/TransactionCurrencySeeder.php index 8f51c69cd1..0d7732e80b 100644 --- a/database/seeds/TransactionCurrencySeeder.php +++ b/database/seeds/TransactionCurrencySeeder.php @@ -28,6 +28,7 @@ class TransactionCurrencySeeder extends Seeder TransactionCurrency::create(['code' => 'GBP', 'name' => 'British Pound', 'symbol' => '£', 'decimal_places' => 2]); TransactionCurrency::create(['code' => 'IDR', 'name' => 'Indonesian rupiah', 'symbol' => 'Rp', 'decimal_places' => 2]); TransactionCurrency::create(['code' => 'XBT', 'name' => 'Bitcoin', 'symbol' => 'B', 'decimal_places' => 8]); + TransactionCurrency::create(['code' => 'JPY', 'name' => 'Japanese yen', 'symbol' => '¥', 'decimal_places' => 2]); } }