Merge branch 'feature/csv-import' into develop

Conflicts:
	composer.json
	composer.lock
	resources/lang/en/form.php
	resources/lang/nl/form.php
This commit is contained in:
James Cole
2015-07-05 08:50:54 +02:00
42 changed files with 2347 additions and 951 deletions

View File

@@ -0,0 +1,51 @@
<?php
/**
* Created by PhpStorm.
* User: sander
* Date: 05/07/15
* Time: 05:49
*/
namespace FireflyIII\Helpers\Csv\Converter;
use Auth;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType;
use Log;
/**
* Class AccountIban
*
* @package FireflyIII\Helpers\Csv\Converter
*/
class AccountIban extends BasicConverter implements ConverterInterface
{
/**
* @return Account|null
*/
public function convert()
{
// is mapped? Then it's easy!
if (isset($this->mapped[$this->index][$this->value])) {
$account = Auth::user()->accounts()->find($this->mapped[$this->index][$this->value]);
} else {
// find or create new account:
$accountType = AccountType::where('type', 'Asset account')->first();
$account = Account::firstOrCreateEncrypted(
[
'name' => $this->value,
//'iban' => $this->value,
'user_id' => Auth::user()->id,
'account_type_id' => $accountType->id,
'active' => true,
]
);
if ($account->getErrors()->count() > 0) {
Log::error('Create or find asset account: ' . json_encode($account->getErrors()->all()));
}
}
return $account;
}
}

View File

@@ -0,0 +1,32 @@
<?php
/**
* Created by PhpStorm.
* User: sander
* Date: 05/07/15
* Time: 05:49
*/
namespace FireflyIII\Helpers\Csv\Converter;
use FireflyIII\Models\Account;
/**
* Class Amount
*
* @package FireflyIII\Helpers\Csv\Converter
*/
class Amount extends BasicConverter implements ConverterInterface
{
/**
* @return Account|null
*/
public function convert()
{
if (is_numeric($this->value)) {
return $this->value;
}
return 0;
}
}

View File

@@ -0,0 +1,122 @@
<?php
namespace FireflyIII\Helpers\Csv\Converter;
/**
* Class BasicConverter
*
* @package FireflyIII\Helpers\Csv\Converter
*/
class BasicConverter
{
/** @var array */
protected $data;
/** @var string */
protected $field;
/** @var int */
protected $index;
/** @var array */
protected $mapped;
/** @var string */
protected $role;
/** @var string */
protected $value;
/**
* @return array
*/
public function getData()
{
return $this->data;
}
/**
* @param array $data
*/
public function setData(array $data)
{
$this->data = $data;
}
/**
* @return string
*/
public function getField()
{
return $this->field;
}
/**
* @param string $field
*/
public function setField($field)
{
$this->field = $field;
}
/**
* @return int
*/
public function getIndex()
{
return $this->index;
}
/**
* @param int $index
*/
public function setIndex($index)
{
$this->index = $index;
}
/**
* @return array
*/
public function getMapped()
{
return $this->mapped;
}
/**
* @param array $mapped
*/
public function setMapped($mapped)
{
$this->mapped = $mapped;
}
/**
* @return string
*/
public function getRole()
{
return $this->role;
}
/**
* @param string $role
*/
public function setRole($role)
{
$this->role = $role;
}
/**
* @return string
*/
public function getValue()
{
return $this->value;
}
/**
* @param string $value
*/
public function setValue($value)
{
$this->value = $value;
}
}

View File

@@ -0,0 +1,55 @@
<?php
/**
* Created by PhpStorm.
* User: sander
* Date: 05/07/15
* Time: 05:42
*/
namespace FireflyIII\Helpers\Csv\Converter;
/**
* Interface ConverterInterface
*
* @package FireflyIII\Helpers\Csv\Converter
*/
interface ConverterInterface
{
/**
* @return mixed
*/
public function convert();
/**
* @param int $index
*/
public function setIndex($index);
/**
* @param array $mapped
*/
public function setMapped($mapped);
/**
* @param string $role
*/
public function setRole($role);
/**
* @param string $value
*/
public function setValue($value);
/**
* @param array $data
*/
public function setData(array $data);
/**
* @param string $field
*
*/
public function setField($field);
}

View File

@@ -0,0 +1,27 @@
<?php
namespace FireflyIII\Helpers\Csv\Converter;
use FireflyIII\Models\TransactionCurrency;
/**
* Class CurrencyCode
*
* @package FireflyIII\Helpers\Csv\Converter
*/
class CurrencyCode extends BasicConverter implements ConverterInterface
{
/**
* @return mixed|static
*/
public function convert()
{
if (isset($this->mapped[$this->index][$this->value])) {
$currency = TransactionCurrency::find($this->mapped[$this->index][$this->value]);
} else {
$currency = TransactionCurrency::whereCode($this->value)->first();
}
return $currency;
}
}

View File

@@ -0,0 +1,33 @@
<?php
/**
* Created by PhpStorm.
* User: sander
* Date: 05/07/15
* Time: 05:49
*/
namespace FireflyIII\Helpers\Csv\Converter;
use Carbon\Carbon;
use Session;
/**
* Class Date
*
* @package FireflyIII\Helpers\Csv\Converter
*/
class Date extends BasicConverter implements ConverterInterface
{
/**
* @return Carbon
*/
public function convert()
{
$format = Session::get('csv-date-format');
$date = Carbon::createFromFormat($format, $this->value);
return $date;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace FireflyIII\Helpers\Csv\Converter;
/**
* Class Description
*
* @package FireflyIII\Helpers\Csv\Converter
*/
class Description extends BasicConverter implements ConverterInterface
{
/**
* @return mixed
*/
public function convert()
{
return trim($this->data['description'] . ' ' . $this->value);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace FireflyIII\Helpers\Csv\Converter;
use FireflyIII\Models\Account;
/**
* Class Amount
*
* @package FireflyIII\Helpers\Csv\Converter
*/
class Ignore extends BasicConverter implements ConverterInterface
{
/**
* @return Account|null
*/
public function convert()
{
return null;
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace FireflyIII\Helpers\Csv\Converter;
/**
* Class OpposingName
*
* @package FireflyIII\Helpers\Csv\Converter
*/
class OpposingName extends BasicConverter implements ConverterInterface
{
/**
* This method cannot search yet for the correct account (Expense account or Revenue account) because simply put,
* Firefly doesn't know yet if this account needs to be an Expense account or a Revenue account. This depends
* on the amount which is in the current row and that's a big unknown.
*
* @return mixed
*/
public function convert()
{
return $this->value;
}
}

View File

@@ -0,0 +1,27 @@
<?php
/**
* Created by PhpStorm.
* User: sander
* Date: 05/07/15
* Time: 06:12
*/
namespace FireflyIII\Helpers\Csv\Converter;
class RabobankDebetCredit extends BasicConverter implements ConverterInterface
{
/**
* @return mixed
*/
public function convert()
{
if ($this->value == 'D') {
return -1;
}
return 1;
}
}

235
app/Helpers/Csv/Data.php Normal file
View File

@@ -0,0 +1,235 @@
<?php
namespace FireflyIII\Helpers\Csv;
use Crypt;
use League\Csv\Reader;
use Session;
/**
* Class Data
*
* @package FireflyIII\Helpers\Csv
*/
class Data
{
/** @var string */
protected $csvFileContent;
/** @var string */
protected $csvFileLocation;
/** @var string */
protected $dateFormat;
/** @var bool */
protected $hasHeaders;
/** @var array */
protected $map;
/** @var array */
protected $mapped;
/** @var Reader */
protected $reader;
/** @var array */
protected $roles;
/**
*
*/
public function __construct()
{
$this->sessionHasHeaders();
$this->sessionDateFormat();
$this->sessionCsvFileLocation();
$this->sessionMap();
$this->sessionRoles();
$this->sessionMapped();
}
protected function sessionHasHeaders()
{
if (Session::has('csv-has-headers')) {
$this->hasHeaders = (bool)Session::get('csv-has-headers');
}
}
protected function sessionDateFormat()
{
if (Session::has('csv-date-format')) {
$this->dateFormat = (string)Session::get('csv-date-format');
}
}
protected function sessionCsvFileLocation()
{
if (Session::has('csv-file')) {
$this->csvFileLocation = (string)Session::get('csv-file');
}
}
protected function sessionMap()
{
if (Session::has('csv-map')) {
$this->map = (array)Session::get('csv-map');
}
}
protected function sessionRoles()
{
if (Session::has('csv-roles')) {
$this->roles = (array)Session::get('csv-roles');
}
}
protected function sessionMapped()
{
if (Session::has('csv-mapped')) {
$this->mapped = (array)Session::get('csv-mapped');
}
}
/**
* @return string
*/
public function getDateFormat()
{
return $this->dateFormat;
}
/**
* @param mixed $dateFormat
*/
public function setDateFormat($dateFormat)
{
Session::put('csv-date-format', $dateFormat);
$this->dateFormat = $dateFormat;
}
/**
* @return bool
*/
public function getHasHeaders()
{
return $this->hasHeaders;
}
/**
* @param bool $hasHeaders
*/
public function setHasHeaders($hasHeaders)
{
Session::put('csv-has-headers', $hasHeaders);
$this->hasHeaders = $hasHeaders;
}
/**
* @return array
*/
public function getMap()
{
return $this->map;
}
/**
* @param array $map
*/
public function setMap(array $map)
{
Session::put('csv-map', $map);
$this->map = $map;
}
/**
* @return array
*/
public function getMapped()
{
return $this->mapped;
}
/**
* @param array $mapped
*/
public function setMapped(array $mapped)
{
Session::put('csv-mapped', $mapped);
$this->mapped = $mapped;
}
/**
* @return Reader
*/
public function getReader()
{
if (strlen($this->csvFileContent) === 0) {
$this->loadCsvFile();
}
if (is_null($this->reader)) {
$this->reader = Reader::createFromString($this->getCsvFileContent());
}
return $this->reader;
}
protected function loadCsvFile()
{
$file = $this->getCsvFileLocation();
$content = file_get_contents($file);
$contentDecrypted = Crypt::decrypt($content);
$this->setCsvFileContent($contentDecrypted);
}
/**
* @return string
*/
public function getCsvFileLocation()
{
return $this->csvFileLocation;
}
/**
* @param string $csvFileLocation
*/
public function setCsvFileLocation($csvFileLocation)
{
Session::put('csv-file', $csvFileLocation);
$this->csvFileLocation = $csvFileLocation;
}
/**
* @return string
*/
public function getCsvFileContent()
{
return $this->csvFileContent;
}
/**
* @param string $csvFileContent
*/
public function setCsvFileContent($csvFileContent)
{
$this->csvFileContent = $csvFileContent;
}
/**
* @return array
*/
public function getRoles()
{
return $this->roles;
}
/**
* @param array $roles
*/
public function setRoles(array $roles)
{
Session::put('csv-roles', $roles);
$this->roles = $roles;
}
}

View File

@@ -0,0 +1,274 @@
<?php
namespace FireflyIII\Helpers\Csv;
use App;
use Auth;
use Carbon\Carbon;
use Config;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Csv\Converter\ConverterInterface;
use FireflyIII\Models\Account;
use FireflyIII\Models\AccountType;
use FireflyIII\Models\Transaction;
use FireflyIII\Models\TransactionJournal;
use FireflyIII\Models\TransactionType;
use Illuminate\Support\MessageBag;
use Log;
set_time_limit(0);
/**
* Class Importer
*
* @package FireflyIII\Helpers\Csv
*/
class Importer
{
/** @var Data */
protected $data;
/** @var array */
protected $errors;
/** @var array */
protected $map;
/** @var array */
protected $mapped;
/** @var array */
protected $roles;
/**
*
*/
public function run()
{
$this->map = $this->data->getMap();
$this->roles = $this->data->getRoles();
$this->mapped = $this->data->getMapped();
foreach ($this->data->getReader() as $index => $row) {
Log::debug('Now at row ' . $index);
$result = $this->importRow($row);
if (!($result === true)) {
Log::error('Caught error at row #' . $index . ': ' . $result);
$this->errors[$index] = $result;
}
}
return count($this->errors);
}
/**
* @param $row
*
* @throws FireflyException
* @return string|bool
*/
protected function importRow($row)
{
/*
* These fields are necessary to create a new transaction journal. Some are optional:
*/
$data = $this->getFiller();
foreach ($row as $index => $value) {
$role = isset($this->roles[$index]) ? $this->roles[$index] : '_ignore';
$class = Config::get('csv.roles.' . $role . '.converter');
$field = Config::get('csv.roles.' . $role . '.field');
if (is_null($class)) {
throw new FireflyException('No converter for field of type "' . $role . '".');
}
if (is_null($field)) {
throw new FireflyException('No place to store value of type "' . $role . '".');
}
/** @var ConverterInterface $converter */
$converter = App::make('FireflyIII\Helpers\Csv\Converter\\' . $class);
$converter->setData($data); // the complete array so far.
$converter->setField($field);
$converter->setIndex($index);
$converter->setMapped($this->mapped);
$converter->setValue($value);
$converter->setRole($role);
$data[$field] = $converter->convert();
}
$data = $this->postProcess($data, $row);
$result = $this->validateData($data);
if ($result === true) {
$result = $this->createTransactionJournal($data);
} else {
Log::error('Validator: ' . $result);
}
if ($result instanceof TransactionJournal) {
return true;
}
return 'Not a journal.';
}
/**
* @return array
*/
protected function getFiller()
{
return [
'description' => '',
'asset-account' => null,
'opposing-account' => '',
'opposing-account-object' => null,
'date' => null,
'currency' => null,
'amount' => null,
'amount-modifier' => 1,
'ignored' => null,
'date-rent' => null,
];
}
/**
* Row denotes the original data.
*
* @param array $data
* @param array $row
*
* @return array
*/
protected function postProcess(array $data, array $row)
{
bcscale(2);
$data['description'] = trim($data['description']);
$data['amount'] = bcmul($data['amount'], $data['amount-modifier']);
if ($data['amount'] < 0) {
// create expense account:
$accountType = AccountType::where('type', 'Expense account')->first();
} else {
// create revenue account:
$accountType = AccountType::where('type', 'Revenue account')->first();
}
if(strlen($data['description']) == 0) {
$data['description'] = trans('firefly.csv_empty_description');
}
// do bank specific fixes:
$specifix = new Specifix();
$specifix->setData($data);
$specifix->setRow($row);
$specifix->fix($data, $row);
// get data back:
$data = $specifix->getData();
$data['opposing-account-object'] = Account::firstOrCreateEncrypted(
[
'user_id' => Auth::user()->id,
'name' => ucwords($data['opposing-account']),
'account_type_id' => $accountType->id,
'active' => 1,
]
);
return $data;
}
/**
* @param $data
*
* @return bool|string
*/
protected function validateData($data)
{
if (is_null($data['date']) && is_null($data['date-rent'])) {
return 'No date value for this row.';
}
if (strlen($data['description']) == 0) {
return 'No valid description';
}
if (is_null($data['opposing-account-object'])) {
return 'Opposing account is null';
}
return true;
}
/**
* @param array $data
*
* @return static
*/
protected function createTransactionJournal(array $data)
{
bcscale(2);
$date = $data['date'];
if (is_null($data['date'])) {
$date = $data['date-rent'];
}
if ($data['amount'] < 0) {
$transactionType = TransactionType::where('type', 'Withdrawal')->first();
} else {
$transactionType = TransactionType::where('type', 'Deposit')->first();
}
$errors = new MessageBag;
$journal = TransactionJournal::create(
[
'user_id' => Auth::user()->id,
'transaction_type_id' => $transactionType->id,
'bill_id' => null,
'transaction_currency_id' => $data['currency']->id,
'description' => $data['description'],
'completed' => 0,
'date' => $date,
]
);
$errors = $journal->getErrors()->merge($errors);
if ($journal->getErrors()->count() == 0) {
// create both transactions:
$transaction = Transaction::create(
[
'transaction_journal_id' => $journal->id,
'account_id' => $data['asset-account']->id,
'amount' => $data['amount']
]
);
$errors = $transaction->getErrors()->merge($errors);
$transaction = Transaction::create(
[
'transaction_journal_id' => $journal->id,
'account_id' => $data['opposing-account-object']->id,
'amount' => bcmul($data['amount'], -1)
]
);
$errors = $transaction->getErrors()->merge($errors);
}
if ($errors->count() == 0) {
$journal->completed = 1;
$journal->save();
}
return $journal;
}
/**
* @param Data $data
*/
public function setData($data)
{
$this->data = $data;
}
/**
* @param $value
*
* @return Carbon
*/
protected function parseDate($value)
{
return Carbon::createFromFormat($this->data->getDateFormat(), $value);
}
}

View File

@@ -0,0 +1,42 @@
<?php
/**
* Created by PhpStorm.
* User: sander
* Date: 05/07/15
* Time: 08:35
*/
namespace FireflyIII\Helpers\Csv\Mapper;
use Auth;
use FireflyIII\Models\Account;
use Illuminate\Database\Eloquent\Relations\HasMany;
/**
* Class AssetAccount
*
* @package FireflyIII\Helpers\Csv\Mapper
*/
class AssetAccount implements MapperInterface
{
/**
* @return array
*/
public function getMap()
{
$result = Auth::user()->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) {
$list[$account->id] = $account->name;
}
return $list;
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace FireflyIII\Helpers\Csv\Mapper;
/**
* Interface MapperInterface
*
* @package FireflyIII\Helpers\Csv\Mapper
*/
interface MapperInterface
{
/**
* @return array
*/
public function getMap();
}

View File

@@ -0,0 +1,28 @@
<?php
namespace FireflyIII\Helpers\Csv\Mapper;
use FireflyIII\Models\TransactionCurrency as TC;
/**
* Class TransactionCurrency
*
* @package FireflyIII\Helpers\Csv\Mapper
*/
class TransactionCurrency implements MapperInterface
{
/**
* @return array
*/
public function getMap()
{
$currencies = TC::get();
$list = [];
foreach ($currencies as $currency) {
$list[$currency->id] = $currency->name . ' (' . $currency->code . ')';
}
return $list;
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace FireflyIII\Helpers\Csv;
/**
* Class Specifix
*
* @package FireflyIII\Helpers\Csv
*/
class Specifix
{
/** @var array */
protected $data;
/** @var array */
protected $row;
/**
* Implement bank and locale related fixes.
*/
public function fix()
{
$this->rabobankFixEmptyOpposing();
}
/**
* Fixes Rabobank specific thing.
*/
protected function rabobankFixEmptyOpposing()
{
if (strlen($this->data['opposing-account']) == 0) {
$this->data['opposing-account'] = $this->row[10];
}
$this->data['description'] = trim(str_replace($this->row[10], '', $this->data['description']));
}
/**
* @return array
*/
public function getData()
{
return $this->data;
}
/**
* @param array $data
*/
public function setData($data)
{
$this->data = $data;
}
/**
* @return array
*/
public function getRow()
{
return $this->row;
}
/**
* @param array $row
*/
public function setRow($row)
{
$this->row = $row;
}
}

171
app/Helpers/Csv/Wizard.php Normal file
View File

@@ -0,0 +1,171 @@
<?php
namespace FireflyIII\Helpers\Csv;
use App;
use Auth;
use Config;
use Crypt;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Csv\Mapper\MapperInterface;
use League\Csv\Reader;
use ReflectionException;
use Session;
/**
* Class Wizard
*
* @package FireflyIII\Helpers\Csv
*/
class Wizard implements WizardInterface
{
/**
* @param Reader $reader
* @param array $map
* @param bool $hasHeaders
*
* @return array
*/
public function getMappableValues($reader, array $map, $hasHeaders)
{
$values = [];
/*
* Loop over the CSV and collect mappable data:
*/
foreach ($reader as $index => $row) {
if (($hasHeaders && $index > 1) || !$hasHeaders) {
// collect all map values
foreach ($map as $column => $irrelevant) {
// check if $irrelevant is mappable!
$values[$column][] = $row[$column];
}
}
}
/*
* Make each one unique.
*/
foreach ($values as $column => $found) {
$values[$column] = array_unique($found);
}
return $values;
}
/**
* @param array $roles
* @param mixed $map
*
* @return array
*/
public function processSelectedMapping(array $roles, $map)
{
$configRoles = Config::get('csv.roles');
$maps = [];
if (is_array($map)) {
foreach ($map as $index => $field) {
if (isset($roles[$index])) {
$name = $roles[$index];
if ($configRoles[$name]['mappable']) {
$maps[$index] = $name;
}
}
}
}
return $maps;
}
/**
* @param mixed $input
*
* @return array
*/
public function processSelectedRoles($input)
{
$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)
{
foreach ($fields as $field) {
if (!Session::has($field)) {
return false;
}
}
return true;
}
/**
* @param array $map
*
* @return array
* @throws FireflyException
*/
public function showOptions(array $map)
{
$options = [];
foreach ($map as $index => $columnRole) {
$mapper = Config::get('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::make($class);
} catch (ReflectionException $e) {
throw new FireflyException('Column "' . $columnRole . '" cannot be mapped because class ' . $mapper . ' does not exist.');
}
$set = $mapObject->getMap();
$options[$index] = $set;
}
return $options;
}
/**
* @param $path
*
* @return string
*/
public function storeCsvFile($path)
{
$time = str_replace(' ', '-', microtime());
$fileName = 'csv-upload-' . Auth::user()->id . '-' . $time . '.csv.encrypted';
$fullPath = storage_path('upload') . DIRECTORY_SEPARATOR . $fileName;
$content = file_get_contents($path);
$contentEncrypted = Crypt::encrypt($content);
file_put_contents($fullPath, $contentEncrypted);
return $fullPath;
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace FireflyIII\Helpers\Csv;
use League\Csv\Reader;
/**
* Interface WizardInterface
*
* @package FireflyIII\Helpers\Csv
*/
interface WizardInterface
{
/**
* @param Reader $reader
* @param array $map
* @param bool $hasHeaders
*
* @return array
*/
public function getMappableValues($reader, array $map, $hasHeaders);
/**
* @param array $roles
* @param mixed $map
*
* @return array
*/
public function processSelectedMapping(array $roles, $map);
/**
* @param mixed $input
*
* @return array
*/
public function processSelectedRoles($input);
/**
* @param array $fields
*
* @return bool
*/
public function sessionHasValues(array $fields);
/**
* @param array $map
*
* @return array
*/
public function showOptions(array $map);
/**
* @param $path
*
* @return string
*/
public function storeCsvFile($path);
}

View File

@@ -0,0 +1,413 @@
<?php
/**
* Created by PhpStorm.
* User: sander
* Date: 03/07/15
* Time: 10:37
*/
namespace FireflyIII\Http\Controllers;
use App;
use Config;
use FireflyIII\Exceptions\FireflyException;
use FireflyIII\Helpers\Csv\Data;
use FireflyIII\Helpers\Csv\Importer;
use FireflyIII\Helpers\Csv\WizardInterface;
use Illuminate\Http\Request;
use Input;
use Log;
use Redirect;
use Session;
use View;
/**
* Class CsvController
*
* @package FireflyIII\Http\Controllers
*/
class CsvController extends Controller
{
/** @var Data */
protected $data;
/** @var WizardInterface */
protected $wizard;
/**
*
*/
public function __construct()
{
parent::__construct();
View::share('title', trans('firefly.csv'));
View::share('mainTitleIcon', 'fa-file-text-o');
if (Config::get('firefly.csv_import_enabled') === false) {
throw new FireflyException('CSV Import is not enabled.');
}
$this->wizard = App::make('FireflyIII\Helpers\Csv\WizardInterface');
$this->data = App::make('FireflyIII\Helpers\Csv\Data');
}
/**
* Define column roles and mapping.
*
*
* STEP THREE
*
* @return View
*/
public function columnRoles()
{
$fields = ['csv-file', 'csv-date-format', 'csv-has-headers'];
if (!$this->wizard->sessionHasValues($fields)) {
Session::flash('warning', 'Could not recover upload.');
return Redirect::route('csv.index');
}
$subTitle = trans('firefly.csv_process');
$firstRow = $this->data->getReader()->fetchOne();
$count = count($firstRow);
$headers = [];
$example = $this->data->getReader()->fetchOne();
$availableRoles = [];
$roles = $this->data->getRoles();
$map = $this->data->getMap();
for ($i = 1; $i <= $count; $i++) {
$headers[] = trans('firefly.csv_column') . ' #' . $i;
}
if ($this->data->getHasHeaders()) {
$headers = $firstRow;
}
foreach (Config::get('csv.roles') as $name => $role) {
$availableRoles[$name] = $role['name'];
}
ksort($availableRoles);
return view('csv.column-roles', compact('availableRoles', 'map', 'roles', 'headers', 'example', 'subTitle'));
}
/**
* Optional download of mapping.
*
* STEP FOUR THREE-A
*/
public function downloadConfig()
{
$fields = ['csv-date-format', 'csv-has-headers'];
if (!$this->wizard->sessionHasValues($fields)) {
Session::flash('warning', 'Could not recover upload.');
return Redirect::route('csv.index');
}
$data = [
'date-format' => Session::get('date-format'),
'has-headers' => Session::get('csv-has-headers')
];
// $fields = ['csv-file', 'csv-date-format', 'csv-has-headers', 'csv-map', 'csv-roles', 'csv-mapped'];
if (Session::has('csv-map')) {
$data['map'] = Session::get('csv-map');
}
if (Session::has('csv-roles')) {
$data['roles'] = Session::get('csv-roles');
}
if (Session::has('csv-mapped')) {
$data['mapped'] = Session::get('csv-mapped');
}
$result = json_encode($data, JSON_PRETTY_PRINT);
$name = 'csv-configuration-' . date('Y-m-d') . '.json';
header('Content-disposition: attachment; filename=' . $name);
header('Content-type: application/json');
echo $result;
exit;
}
/**
* @return View
*/
public function downloadConfigPage()
{
return view('csv.download-config');
}
/**
* This method shows the initial upload form.
*
* STEP ONE
*
* @return View
*/
public function index()
{
$subTitle = trans('firefly.csv_import');
Session::forget('csv-date-format');
Session::forget('csv-has-headers');
Session::forget('csv-file');
Session::forget('csv-map');
Session::forget('csv-roles');
Session::forget('csv-mapped');
// get values which are yet unsaveable or unmappable:
$unsupported = [];
foreach (Config::get('csv.roles') as $role) {
if (!isset($role['converter'])) {
$unsupported[] = trans('firefly.csv_unsupported_value', ['columnRole' => $role['name']]);
}
if ($role['mappable'] === true && !isset($role['mapper'])) {
$unsupported[] = trans('firefly.csv_unsupported_map', ['columnRole' => $role['name']]);
}
}
sort($unsupported);
// can actually upload?
$uploadPossible = is_writable(storage_path('upload'));
$path = storage_path('upload');
return view('csv.index', compact('subTitle', 'uploadPossible', 'path', 'unsupported'));
}
/**
* Parse the file.
*
* STEP FOUR
*
* @return \Illuminate\Http\RedirectResponse
*/
public function initialParse()
{
$fields = ['csv-file', 'csv-date-format', 'csv-has-headers'];
if (!$this->wizard->sessionHasValues($fields)) {
Session::flash('warning', 'Could not recover upload.');
return Redirect::route('csv.index');
}
// process given roles and mapping:
$roles = $this->wizard->processSelectedRoles(Input::get('role'));
$maps = $this->wizard->processSelectedMapping($roles, Input::get('map'));
Session::put('csv-map', $maps);
Session::put('csv-roles', $roles);
/*
* Go back when no roles defined:
*/
if (count($roles) === 0) {
Session::flash('warning', 'Please select some roles.');
return Redirect::route('csv.column-roles');
}
/*
* Continue with map specification when necessary.
*/
if (count($maps) > 0) {
return Redirect::route('csv.map');
}
/*
* Or simply start processing.
*/
// proceed to download config
return Redirect::route('csv.download-config-page');
}
/**
*
* Map first if necessary,
*
* STEP FIVE.
*
* @return \Illuminate\Http\RedirectResponse|View
* @throws FireflyException
*/
public function map()
{
/*
* Make sure all fields we need are accounted for.
*/
$fields = ['csv-file', 'csv-date-format', 'csv-has-headers', 'csv-map', 'csv-roles'];
if (!$this->wizard->sessionHasValues($fields)) {
Session::flash('warning', 'Could not recover upload.');
return Redirect::route('csv.index');
}
/*
* The "options" array contains all options the user has
* per column, where the key represents the column.
*
* For each key there is an array which in turn represents
* all the options available: grouped by ID.
*
* Aka:
*
* options[column index] = [
* field id => field identifier.
* ]
*/
try {
$options = $this->wizard->showOptions($this->data->getMap());
} catch (FireflyException $e) {
return view('error', ['message' => $e->getMessage()]);
}
/*
* After these values are prepped, read the actual CSV file
*/
$reader = $this->data->getReader();
$map = $this->data->getMap();
$hasHeaders = $this->data->getHasHeaders();
$values = $this->wizard->getMappableValues($reader, $map, $hasHeaders);
$map = $this->data->getMap();
$mapped = $this->data->getMapped();
return view('csv.map', compact('map', 'options', 'values', 'mapped'));
}
/**
* Finally actually process the CSV file.
*
* STEP SEVEN
*/
public function process()
{
/*
* Make sure all fields we need are accounted for.
*/
$fields = ['csv-file', 'csv-date-format', 'csv-has-headers', 'csv-map', 'csv-roles', 'csv-mapped'];
if (!$this->wizard->sessionHasValues($fields)) {
Session::flash('warning', 'Could not recover upload.');
return Redirect::route('csv.index');
}
Log::debug('Created importer');
$importer = new Importer;
$importer->setData($this->data);
try {
$importer->run();
} catch (FireflyException $e) {
Log::error('Catch error: ' . $e->getMessage());
return view('error', ['message' => $e->getMessage()]);
}
Log::debug('Done importing!');
echo 'display result';
exit;
}
/**
* Store the mapping the user has made. This is
*
* STEP SIX
*/
public function saveMapping()
{
/*
* Make sure all fields we need are accounted for.
*/
$fields = ['csv-file', 'csv-date-format', 'csv-has-headers', 'csv-map', 'csv-roles'];
if (!$this->wizard->sessionHasValues($fields)) {
Session::flash('warning', 'Could not recover upload.');
return Redirect::route('csv.index');
}
// save mapping to session.
$mapped = [];
if (!is_array(Input::get('mapping'))) {
Session::flash('warning', 'Invalid mapping.');
return Redirect::route('csv.map');
}
foreach (Input::get('mapping') as $index => $data) {
$mapped[$index] = [];
foreach ($data as $value => $mapping) {
$mapped[$index][$value] = $mapping;
}
}
Session::put('csv-mapped', $mapped);
// proceed to process.
return Redirect::route('csv.download-config-page');
}
/**
*
* This method processes the file, puts it away somewhere safe
* and sends you onwards.
*
* STEP TWO
*
* @param Request $request
*
* @return \Illuminate\Http\RedirectResponse
*/
public function upload(Request $request)
{
if (!$request->hasFile('csv')) {
Session::flash('warning', 'No file uploaded.');
return Redirect::route('csv.index');
}
/*
* Store CSV and put in session.
*/
$fullPath = $this->wizard->storeCsvFile($request->file('csv')->getRealPath());
$dateFormat = Input::get('date_format');
$hasHeaders = intval(Input::get('has_headers')) === 1;
$map = [];
$roles = [];
$mapped = [];
/*
* Process config file if present.
*/
if ($request->hasFile('csv_config')) {
$data = file_get_contents($request->file('csv_config')->getRealPath());
$json = json_decode($data, true);
if (!is_null($json)) {
$dateFormat = isset($json['date-format']) ? $json['date-format'] : $dateFormat;
$hasHeaders = isset($json['has-headers']) ? $json['has-headers'] : $hasHeaders;
$map = isset($json['map']) && is_array($json['map']) ? $json['map'] : [];
$mapped = isset($json['mapped']) && is_array($json['mapped']) ? $json['mapped'] : [];
$roles = isset($json['roles']) && is_array($json['roles']) ? $json['roles'] : [];
}
}
$this->data->setCsvFileLocation($fullPath);
$this->data->setDateFormat($dateFormat);
$this->data->setHasHeaders($hasHeaders);
$this->data->setMap($map);
$this->data->setMapped($mapped);
$this->data->setRoles($roles);
return Redirect::route('csv.column-roles');
}
}

View File

@@ -219,6 +219,21 @@ Route::group(
Route::post('/categories/update/{category}', ['uses' => 'CategoryController@update', 'as' => 'categories.update']);
Route::post('/categories/destroy/{category}', ['uses' => 'CategoryController@destroy', 'as' => 'categories.destroy']);
/**
* CSV controller
*/
Route::get('/csv', ['uses' => 'CsvController@index', 'as' => 'csv.index']);
Route::post('/csv/upload', ['uses' => 'CsvController@upload', 'as' => 'csv.upload']);
Route::get('/csv/column_roles', ['uses' => 'CsvController@columnRoles', 'as' => 'csv.column-roles']);
Route::post('/csv/initial_parse', ['uses' => 'CsvController@initialParse', 'as' => 'csv.initial_parse']);
Route::get('/csv/map', ['uses' => 'CsvController@map', 'as' => 'csv.map']);
Route::get('/csv/download-config', ['uses' => 'CsvController@downloadConfig', 'as' => 'csv.download-config']);
Route::get('/csv/download', ['uses' => 'CsvController@downloadConfigPage', 'as' => 'csv.download-config-page']);
Route::post('/csv/save_mapping', ['uses' => 'CsvController@saveMapping', 'as' => 'csv.save_mapping']);
Route::get('/csv/process', ['uses' => 'CsvController@process', 'as' => 'csv.process']);
/**
* Currency Controller
*/

View File

@@ -91,6 +91,9 @@ class FireflyServiceProvider extends ServiceProvider
$this->app->bind('FireflyIII\Repositories\Tag\TagRepositoryInterface', 'FireflyIII\Repositories\Tag\TagRepository');
$this->app->bind('FireflyIII\Support\Search\SearchInterface', 'FireflyIII\Support\Search\Search');
// CSV import
$this->app->bind('FireflyIII\Helpers\Csv\WizardInterface', 'FireflyIII\Helpers\Csv\Wizard');
// make charts:
// alternative is Google instead of ChartJs
$this->app->bind('FireflyIII\Generator\Chart\Account\AccountChartGenerator', 'FireflyIII\Generator\Chart\Account\ChartJsAccountChartGenerator');

View File

@@ -347,6 +347,24 @@ class ExpandedForm
return $html;
}
/**
* @param $name
* @param null $value
* @param array $options
*
* @return string
*/
public function file($name, array $options = [])
{
$label = $this->label($name, $options);
$options = $this->expandOptionArray($name, $label, $options);
$classes = $this->getHolderClasses($name);
$html = View::make('form.file', compact('classes', 'name', 'label', 'options'))->render();
return $html;
}
/**
* @param $name
* @param null $value