From 015935ed5587159d6aca993af52e075434289330 Mon Sep 17 00:00:00 2001 From: James Cole Date: Fri, 12 Aug 2016 12:55:52 +0200 Subject: [PATCH] Implemented ABN Amro specific import code. --- app/Import/Setup/CsvSetup.php | 34 +++- app/Import/Specifics/AbnAmroDescription.php | 192 +++++++++++++++++++- 2 files changed, 212 insertions(+), 14 deletions(-) diff --git a/app/Import/Setup/CsvSetup.php b/app/Import/Setup/CsvSetup.php index 5dc009b90e..cb656e37f5 100644 --- a/app/Import/Setup/CsvSetup.php +++ b/app/Import/Setup/CsvSetup.php @@ -16,6 +16,7 @@ use ExpandedForm; use FireflyIII\Crud\Account\AccountCrud; use FireflyIII\Import\Mapper\MapperInterface; use FireflyIII\Import\MapperPreProcess\PreProcessorInterface; +use FireflyIII\Import\Specifics\SpecificInterface; use FireflyIII\Models\Account; use FireflyIII\Models\AccountType; use FireflyIII\Models\ImportJob; @@ -191,7 +192,7 @@ class CsvSetup implements SetupInterface { /** @var AccountCrud $repository */ $repository = app(AccountCrud::class, [auth()->user()]); - $importId = $data['csv_import_account'] ?? 0; + $importId = $data['csv_import_account'] ?? 0; $account = $repository->find(intval($importId)); $hasHeaders = isset($data['has_headers']) && intval($data['has_headers']) === 1 ? true : false; @@ -199,6 +200,7 @@ class CsvSetup implements SetupInterface $config['has-headers'] = $hasHeaders; $config['date-format'] = $data['date_format']; $config['delimiter'] = $data['csv_delimiter']; + $config['delimiter'] = $config['delimiter'] === 'tab' ? "\t" : $config['delimiter']; Log::debug('Entered import account.', ['id' => $importId]); @@ -355,11 +357,22 @@ class CsvSetup implements SetupInterface // in order to actually map we also need all possible values from the CSV file. $content = $this->job->uploadFileContents(); /** @var Reader $reader */ - $reader = Reader::createFromString($content); + $reader = Reader::createFromString($content); $reader->setDelimiter($config['delimiter']); $results = $reader->fetch(); foreach ($results as $rowIndex => $row) { + + // run specifics here: + // and this is the point where the specifix go to work. + foreach ($config['specifics'] as $name => $enabled) { + /** @var SpecificInterface $specific */ + $specific = app('FireflyIII\Import\Specifics\\' . $name); + + // it returns the row, possibly modified: + $row = $specific->run($row); + } + //do something here foreach ($indexes as $index) { // this is simply 1, 2, 3, etc. $value = $row[$index]; @@ -409,12 +422,23 @@ class CsvSetup implements SetupInterface // create CSV reader. $reader = Reader::createFromString($content); $reader->setDelimiter($config['delimiter']); - $start = $config['has-headers'] ? 1 : 0; - $end = $start + self::EXAMPLE_ROWS; // first X rows + $start = $config['has-headers'] ? 1 : 0; + $end = $start + self::EXAMPLE_ROWS; // first X rows // collect example data in $data['columns'] while ($start < $end) { $row = $reader->fetchOne($start); + + // run specifics here: + // and this is the point where the specifix go to work. + foreach ($config['specifics'] as $name => $enabled) { + /** @var SpecificInterface $specific */ + $specific = app('FireflyIII\Import\Specifics\\' . $name); + + // it returns the row, possibly modified: + $row = $specific->run($row); + } + foreach ($row as $index => $value) { $value = trim($value); if (strlen($value) > 0) { @@ -422,7 +446,7 @@ class CsvSetup implements SetupInterface } } $start++; - $data['columnCount'] = count($row); + $data['columnCount'] = count($row) > $data['columnCount'] ? count($row) : $data['columnCount']; } // make unique example data diff --git a/app/Import/Specifics/AbnAmroDescription.php b/app/Import/Specifics/AbnAmroDescription.php index f6787e11f5..33408e7c77 100644 --- a/app/Import/Specifics/AbnAmroDescription.php +++ b/app/Import/Specifics/AbnAmroDescription.php @@ -14,18 +14,17 @@ namespace FireflyIII\Import\Specifics; /** * Class AbnAmroDescription * + * Parses the description from txt files for ABN AMRO bank accounts. + * + * Based on the logic as described in the following Gist: + * https://gist.github.com/vDorst/68d555a6a90f62fec004 + * * @package FireflyIII\Import\Specifics */ class AbnAmroDescription implements SpecificInterface { - - /** - * @return string - */ - static public function getName(): string - { - return 'ABN Amro description'; - } + /** @var array */ + public $row; /** * @return string @@ -35,6 +34,14 @@ class AbnAmroDescription implements SpecificInterface return 'Fixes possible problems with ABN Amro descriptions.'; } + /** + * @return string + */ + static public function getName(): string + { + return 'ABN Amro description'; + } + /** * @param array $row * @@ -42,6 +49,173 @@ class AbnAmroDescription implements SpecificInterface */ public function run(array $row): array { - return $row; + $this->row = $row; + // 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->row[7] = trans('firefly.unknown'); // opposing-account-name + } + + return $this->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->row[7], $matches)) { + + $this->row[8] = 'ABN AMRO'; // this one is new (opposing account name) + $this->row[7] = $matches[1]; // this is the description + + 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->row[7], $matches)) { + + // description and opposing account will be the same. + $this->row[8] = $matches[4]; // 'opposing-account-name' + $this->row[7] = $matches[4]; // 'description' + + if ($matches[1] == 'GEA') { + $this->row[7] = 'GEA ' . $matches[4]; // 'description' + } + + 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->row[7], $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->row[7], $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->row[8] = $value; + $name = $value; + break; + case 'KENMERK': + $reference = $value; + break; + case 'IBAN': + $this->row[9] = $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->row[7] = $newDescription; + if (strlen($newDescription) === 0) { + $this->row[7] = 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->row[7], $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->row[8] = $name = $value; + break; + case 'REMI': + $newDescription = $value; + break; + case 'IBAN': + $this->row[9] = $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->row[7] = $newDescription; + if (strlen($newDescription) === 0) { + $this->row[7] = sprintf('%s - %s (%s)', $type, $name, $reference); + } + } + + return true; + } + + return false; + } + + } \ No newline at end of file