diff --git a/app/Import/Routine/FileRoutine.php b/app/Import/Routine/FileRoutine.php index 65e77616ea..f39153c91b 100644 --- a/app/Import/Routine/FileRoutine.php +++ b/app/Import/Routine/FileRoutine.php @@ -26,7 +26,6 @@ use FireflyIII\Exceptions\FireflyException; use FireflyIII\Models\ImportJob; use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; use FireflyIII\Support\Import\Routine\File\FileProcessorInterface; -use FireflyIII\Support\Import\Routine\File\FileRoutineInterface; use Log; /** diff --git a/app/Support/Import/Placeholder/ColumnValue.php b/app/Support/Import/Placeholder/ColumnValue.php new file mode 100644 index 0000000000..238a9687bc --- /dev/null +++ b/app/Support/Import/Placeholder/ColumnValue.php @@ -0,0 +1,113 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Support\Import\Placeholder; + +/** + * Class Column + */ +class ColumnValue +{ + /** @var int */ + private $mappedValue; + /** @var string */ + private $originalRole; + /** @var string */ + private $role; + /** @var string */ + private $value; + + /** + * ColumnValue constructor. + */ + public function __construct() + { + $this->mappedValue = 0; + } + + /** + * @return int + */ + public function getMappedValue(): int + { + return $this->mappedValue; + } + + /** + * @param int $mappedValue + */ + public function setMappedValue(int $mappedValue): void + { + $this->mappedValue = $mappedValue; + } + + /** + * @return string + */ + public function getOriginalRole(): string + { + return $this->originalRole; + } + + /** + * @param string $originalRole + */ + public function setOriginalRole(string $originalRole): void + { + $this->originalRole = $originalRole; + } + + /** + * @return string + */ + public function getRole(): string + { + return $this->role; + } + + /** + * @param string $role + */ + public function setRole(string $role): void + { + $this->role = $role; + } + + /** + * @return string + */ + public function getValue(): string + { + return $this->value; + } + + /** + * @param string $value + */ + public function setValue(string $value): void + { + $this->value = $value; + } + + +} \ No newline at end of file diff --git a/app/Support/Import/Placeholder/ImportTransaction.php b/app/Support/Import/Placeholder/ImportTransaction.php new file mode 100644 index 0000000000..57ba4522b6 --- /dev/null +++ b/app/Support/Import/Placeholder/ImportTransaction.php @@ -0,0 +1,220 @@ +. + */ + +declare(strict_types=1); + +namespace FireflyIII\Support\Import\Placeholder; + +use FireflyIII\Exceptions\FireflyException; + +/** + * Class ImportTransaction + */ +class ImportTransaction +{ + /** @var string */ + private $accountBic; + /** @var string */ + private $accountIban; + /** @var int */ + private $accountId; + /** @var string */ + private $accountName; + /** @var string */ + private $amount; + /** @var string */ + private $amountCredit; + /** @var string */ + private $amountDebit; + /** @var int */ + private $billId; + /** @var int */ + private $budgetId; + /** @var int */ + private $categoryId; + /** @var int */ + private $currencyId; + /** @var string */ + private $date; + /** @var string */ + private $description; + /** @var string */ + private $externalId; + /** @var string */ + private $foreignAmount; + /** @var int */ + private $foreignCurrencyId; + /** @var array */ + private $meta; + /** @var array */ + private $modifiers; + /** @var string */ + private $note; + /** @var string */ + private $opposingBic; + /** @var string */ + private $opposingIban; + /** @var int */ + private $opposingId; + /** @var string */ + private $opposingName; + /** @var array */ + private $tags; + + /** + * ImportTransaction constructor. + */ + public function __construct() + { + $this->tags = []; + $this->modifiers = []; + $this->meta = []; + $this->description = ''; + $this->note = ''; + } + + /** + * @param ColumnValue $columnValue + * + * @throws FireflyException + */ + public function addColumnValue(ColumnValue $columnValue): void + { + switch ($columnValue->getRole()) { + default: + throw new FireflyException( + sprintf('ImportTransaction cannot handle role "%s" with value "%s"', $columnValue->getRole(), $columnValue->getValue()) + ); + case 'account-id': + // could be the result of a mapping? + $this->accountId = $this->getMappedValue($columnValue); + break; + case 'account-name': + $this->accountName = $columnValue->getValue(); + break; + case 'sepa-ct-id'; + case 'sepa-ct-op'; + case 'sepa-db'; + case 'sepa-cc': + case 'sepa-country'; + case 'sepa-ep'; + case 'sepa-ci'; + case 'internal-reference': + case 'date-interest': + case 'date-invoice': + case 'date-book': + case 'date-payment': + case 'date-process': + case 'date-due': + $this->meta[$columnValue->getRole()] = $columnValue->getValue(); + break; + case'amount_debit': + $this->amountDebit = $columnValue->getValue(); + break; + case'amount_credit': + $this->amountCredit = $columnValue->getValue(); + break; + case 'amount': + $this->amount = $columnValue->getValue(); + break; + case 'amount_foreign': + $this->foreignAmount = $columnValue->getValue(); + break; + case 'foreign-currency-id': + $this->foreignCurrencyId = $this->getMappedValue($columnValue); + break; + case 'bill-id': + $this->billId = $this->getMappedValue($columnValue); + break; + case 'budget-id': + $this->budgetId = $this->getMappedValue($columnValue); + break; + case 'category-id': + $this->categoryId = $this->getMappedValue($columnValue); + break; + case 'currency-id': + $this->currencyId = $this->getMappedValue($columnValue); + break; + case 'date-transaction': + $this->date = $columnValue->getValue(); + break; + case 'description': + $this->description .= $columnValue->getValue(); + break; + case 'note': + $this->note .= $columnValue->getValue(); + break; + case 'external-id': + $this->externalId = $columnValue->getValue(); + break; + case 'rabo-debit-credit': + case 'ing-debit-credit': + $this->modifiers[$columnValue->getRole()] = $columnValue->getValue(); + break; + case 'opposing-id': + $this->opposingId = $this->getMappedValue($columnValue); + break; + case 'opposing-name': + $this->opposingName = $columnValue->getValue(); + break; + case 'opposing-bic': + $this->opposingBic = $columnValue->getValue(); + break; + case 'tags-comma': + // todo split using pre-processor. + $this->tags = $columnValue->getValue(); + break; + case 'tags-space': + // todo split using pre-processor. + $this->tags = $columnValue->getValue(); + break; + case 'account-iban': + $this->accountIban = $columnValue->getValue(); + break; + case 'opposing-iban': + $this->opposingIban = $columnValue->getValue(); + break; + case '_ignore': + case 'bill-name': + case 'currency-name': + case 'currency-code': + case 'foreign-currency-code': + case 'currency-symbol': + case 'budget-name': + case 'category-name': + case 'account-number': + case 'opposing-number': + } + } + + /** + * Returns the mapped value if it exists in the ColumnValue object. + * + * @param ColumnValue $columnValue + * + * @return int + */ + private function getMappedValue(ColumnValue $columnValue): int + { + return $columnValue->getMappedValue() > 0 ? $columnValue->getMappedValue() : (int)$columnValue->getValue(); + } + +} \ No newline at end of file diff --git a/app/Support/Import/Routine/File/CSVProcessor.php b/app/Support/Import/Routine/File/CSVProcessor.php index ff8476659b..e9a0d873d9 100644 --- a/app/Support/Import/Routine/File/CSVProcessor.php +++ b/app/Support/Import/Routine/File/CSVProcessor.php @@ -28,6 +28,8 @@ use FireflyIII\Helpers\Attachments\AttachmentHelperInterface; use FireflyIII\Models\Attachment; use FireflyIII\Models\ImportJob; use FireflyIII\Repositories\ImportJob\ImportJobRepositoryInterface; +use FireflyIII\Support\Import\Placeholder\ColumnValue; +use FireflyIII\Support\Import\Placeholder\ImportTransaction; use Illuminate\Support\Collection; use League\Csv\Exception; use League\Csv\Reader; @@ -44,8 +46,12 @@ class CSVProcessor implements FileProcessorInterface { /** @var AttachmentHelperInterface */ private $attachments; + /** @var array */ + private $config; /** @var ImportJob */ private $importJob; + /** @var array */ + private $mappedValues; /** @var ImportJobRepositoryInterface */ private $repository; @@ -57,7 +63,7 @@ class CSVProcessor implements FileProcessorInterface */ public function run(): array { - $config = $this->importJob->configuration; + // in order to actually map we also need to read the FULL file. try { @@ -66,14 +72,11 @@ class CSVProcessor implements FileProcessorInterface Log::error($e->getMessage()); throw new FireflyException('Cannot get reader: ' . $e->getMessage()); } - // get mapping from config - $roles = $config['column-roles']; - // get all lines from file: - $lines = $this->getLines($reader, $config); + $lines = $this->getLines($reader); // make import objects, according to their role: - $importables = $this->processLines($lines, $roles); + $importables = $this->processLines($lines); echo '
';
         print_r($importables);
@@ -89,6 +92,7 @@ class CSVProcessor implements FileProcessorInterface
     public function setJob(ImportJob $job): void
     {
         $this->importJob   = $job;
+        $this->config      = $job->configuration;
         $this->repository  = app(ImportJobRepositoryInterface::class);
         $this->attachments = app(AttachmentHelperInterface::class);
         $this->repository->setUser($job->user);
@@ -99,14 +103,13 @@ class CSVProcessor implements FileProcessorInterface
      * Returns all lines from the CSV file.
      *
      * @param Reader $reader
-     * @param array  $config
      *
      * @return array
      * @throws FireflyException
      */
-    private function getLines(Reader $reader, array $config): array
+    private function getLines(Reader $reader): array
     {
-        $offset = isset($config['has-headers']) && $config['has-headers'] === true ? 1 : 0;
+        $offset = isset($this->config['has-headers']) && $this->config['has-headers'] === true ? 1 : 0;
         try {
             $stmt = (new Statement)->offset($offset);
         } catch (Exception $e) {
@@ -146,45 +149,93 @@ class CSVProcessor implements FileProcessorInterface
     }
 
     /**
-     * Process a single column. Return is an array with:
-     * [0 => key, 1 => value]
-     * where the first item is the key under which the value
-     * must be stored, and the second is the value.
+     * If the value in the column is mapped to a certain ID,
+     * the column where this ID must be placed will change.
      *
-     * @param int    $column
-     * @param string $value
-     * @param string $role
+     * For example, if you map role "budget-name" with value "groceries" to 1,
+     * then that should become the budget-id. Not the name.
      *
-     * @return array
+     * @param int $column
+     * @param int $mapped
+     *
+     * @return string
      * @throws FireflyException
      */
-    private function processColumn(int $column, string $value, string $role): array
+    private function getRoleForColumn(int $column, int $mapped): string
     {
+        $roles = $this->config['column-roles'];
+        $role  = $roles[$column] ?? '_ignore';
+        if ($mapped === 0) {
+            Log::debug(sprintf('Column #%d with role "%s" is not mapped.', $column, $role));
+
+            return $role;
+        }
+        if (!(isset($this->config['column-do-mapping'][$column]) && $this->config['column-do-mapping'][$column] === true)) {
+
+            return $role;
+        }
         switch ($role) {
             default:
-                throw new FireflyException(sprintf('Cannot handle role "%s" with value "%s"', $role, $value));
-
-
-                // feed each line into a new class which will process
-            // the line. 
+                throw new FireflyException(sprintf('Cannot indicate new role for mapped role "%s"', $role));
+            case 'account-id':
+            case 'account-name':
+            case 'account-iban':
+            case 'account-number':
+                $newRole = 'account-id';
+                break;
+            case 'foreign-currency-id':
+            case 'foreign-currency-code':
+                $newRole = 'foreign-currency-id';
+                break;
+            case 'bill-id':
+            case 'bill-name':
+                $newRole = 'bill-id';
+                break;
+            case 'currency-id':
+            case 'currency-name':
+            case 'currency-code':
+            case 'currency-symbol':
+                $newRole = 'currency-id';
+                break;
+            case 'budget-id':
+            case 'budget-name':
+                $newRole = 'budget-id';
+                break;
+            case 'category-id':
+            case 'category-name':
+                $newRole = 'category-id';
+                break;
+            case 'opposing-id':
+            case 'opposing-name':
+            case 'opposing-iban':
+            case 'opposing-number':
+                $newRole = 'opposing-id';
+                break;
         }
+        Log::debug(sprintf('Role was "%s", but because of mapping, role becomes "%s"', $role, $newRole));
+
+        // also store the $mapped values in a "mappedValues" array.
+        $this->mappedValues[$newRole] = $mapped;
+
+        return $newRole;
     }
 
+
     /**
      * Process all lines in the CSV file. Each line is processed separately.
      *
      * @param array $lines
-     * @param array $roles
      *
      * @return array
      * @throws FireflyException
      */
-    private function processLines(array $lines, array $roles): array
+    private function processLines(array $lines): array
     {
         $processed = [];
-        foreach ($lines as $line) {
-            $processed[] = $this->processSingleLine($line, $roles);
-
+        $count     = \count($lines);
+        foreach ($lines as $index => $line) {
+            Log::debug(sprintf('Now at line #%d of #%d', $index, $count));
+            $processed[] = $this->processSingleLine($line);
         }
 
         return $processed;
@@ -197,18 +248,34 @@ class CSVProcessor implements FileProcessorInterface
      * @param array $line
      * @param array $roles
      *
-     * @return array
+     * @return ImportTransaction
      * @throws FireflyException
      */
-    private function processSingleLine(array $line, array $roles): array
+    private function processSingleLine(array $line): ImportTransaction
     {
+        $transaction = new ImportTransaction;
         // todo run all specifics on row.
-        $transaction = [];
         foreach ($line as $column => $value) {
-            $value = trim($value);
-            $role  = $roles[$column] ?? '_ignore';
-            [$key, $result] = $this->processColumn($column, $value, $role);
-            // if relevant, find mapped value:
+            $value        = trim($value);
+            $originalRole = $this->config['column-roles'][$column] ?? '_ignore';
+            if (\strlen($value) > 0 && $originalRole !== '_ignore') {
+
+                // is a mapped value present?
+                $mapped = $this->config['column-mapping-config'][$column][$value] ?? 0;
+                // the role might change.
+                $role = $this->getRoleForColumn($column, $mapped);
+
+                $columnValue = new ColumnValue;
+                $columnValue->setValue($value);
+                $columnValue->setRole($role);
+                $columnValue->setMappedValue($mapped);
+                $columnValue->setOriginalRole($originalRole);
+                $transaction->addColumnValue($columnValue);
+
+                // create object that parses this column value.
+
+                Log::debug(sprintf('Now at column #%d (%s), value "%s"', $column, $role, $value));
+            }
         }
 
         return $transaction;