diff --git a/app/Helpers/Csv/Converter/AccountIban.php b/app/Helpers/Csv/Converter/AccountIban.php new file mode 100644 index 0000000000..bbca8ac0ee --- /dev/null +++ b/app/Helpers/Csv/Converter/AccountIban.php @@ -0,0 +1,51 @@ +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; + } +} \ No newline at end of file diff --git a/app/Helpers/Csv/Converter/Amount.php b/app/Helpers/Csv/Converter/Amount.php new file mode 100644 index 0000000000..de2e9d5b08 --- /dev/null +++ b/app/Helpers/Csv/Converter/Amount.php @@ -0,0 +1,32 @@ +value)) { + return $this->value; + } + + return 0; + } +} \ No newline at end of file diff --git a/app/Helpers/Csv/Converter/BasicConverter.php b/app/Helpers/Csv/Converter/BasicConverter.php new file mode 100644 index 0000000000..ba2bffc864 --- /dev/null +++ b/app/Helpers/Csv/Converter/BasicConverter.php @@ -0,0 +1,122 @@ +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; + } + + +} \ No newline at end of file diff --git a/app/Helpers/Csv/Converter/ConverterInterface.php b/app/Helpers/Csv/Converter/ConverterInterface.php new file mode 100644 index 0000000000..1e9eb275e3 --- /dev/null +++ b/app/Helpers/Csv/Converter/ConverterInterface.php @@ -0,0 +1,55 @@ +mapped[$this->index][$this->value])) { + $currency = TransactionCurrency::find($this->mapped[$this->index][$this->value]); + } else { + $currency = TransactionCurrency::whereCode($this->value)->first(); + } + + return $currency; + } +} \ No newline at end of file diff --git a/app/Helpers/Csv/Converter/Date.php b/app/Helpers/Csv/Converter/Date.php new file mode 100644 index 0000000000..df79825a62 --- /dev/null +++ b/app/Helpers/Csv/Converter/Date.php @@ -0,0 +1,33 @@ +value); + + return $date; + } +} \ No newline at end of file diff --git a/app/Helpers/Csv/Converter/Description.php b/app/Helpers/Csv/Converter/Description.php new file mode 100644 index 0000000000..19bede19a0 --- /dev/null +++ b/app/Helpers/Csv/Converter/Description.php @@ -0,0 +1,21 @@ +data['description'] . ' ' . $this->value); + } +} \ No newline at end of file diff --git a/app/Helpers/Csv/Converter/Ignore.php b/app/Helpers/Csv/Converter/Ignore.php new file mode 100644 index 0000000000..b9c7607d47 --- /dev/null +++ b/app/Helpers/Csv/Converter/Ignore.php @@ -0,0 +1,22 @@ +value; + } +} \ No newline at end of file diff --git a/app/Helpers/Csv/Converter/RabobankDebetCredit.php b/app/Helpers/Csv/Converter/RabobankDebetCredit.php new file mode 100644 index 0000000000..758ebf3fc2 --- /dev/null +++ b/app/Helpers/Csv/Converter/RabobankDebetCredit.php @@ -0,0 +1,27 @@ +value == 'D') { + return -1; + } + + return 1; + } +} \ No newline at end of file diff --git a/app/Helpers/Csv/Data.php b/app/Helpers/Csv/Data.php new file mode 100644 index 0000000000..149cbda6e5 --- /dev/null +++ b/app/Helpers/Csv/Data.php @@ -0,0 +1,235 @@ +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; + } + + +} \ No newline at end of file diff --git a/app/Helpers/Csv/Importer.php b/app/Helpers/Csv/Importer.php new file mode 100644 index 0000000000..3f7be24554 --- /dev/null +++ b/app/Helpers/Csv/Importer.php @@ -0,0 +1,274 @@ +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); + } + +} \ No newline at end of file diff --git a/app/Helpers/Csv/Mapper/AssetAccount.php b/app/Helpers/Csv/Mapper/AssetAccount.php new file mode 100644 index 0000000000..d8fe71cc2d --- /dev/null +++ b/app/Helpers/Csv/Mapper/AssetAccount.php @@ -0,0 +1,42 @@ +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; + } +} \ No newline at end of file diff --git a/app/Helpers/Csv/Mapper/MapperInterface.php b/app/Helpers/Csv/Mapper/MapperInterface.php new file mode 100644 index 0000000000..a01785f9a4 --- /dev/null +++ b/app/Helpers/Csv/Mapper/MapperInterface.php @@ -0,0 +1,16 @@ +id] = $currency->name . ' (' . $currency->code . ')'; + } + + return $list; + } +} \ No newline at end of file diff --git a/app/Helpers/Csv/Specifix.php b/app/Helpers/Csv/Specifix.php new file mode 100644 index 0000000000..0e530d4a5d --- /dev/null +++ b/app/Helpers/Csv/Specifix.php @@ -0,0 +1,73 @@ +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; + } + + +} \ No newline at end of file diff --git a/app/Helpers/Csv/Wizard.php b/app/Helpers/Csv/Wizard.php new file mode 100644 index 0000000000..92291e7f45 --- /dev/null +++ b/app/Helpers/Csv/Wizard.php @@ -0,0 +1,171 @@ + $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; + + + } +} \ No newline at end of file diff --git a/app/Helpers/Csv/WizardInterface.php b/app/Helpers/Csv/WizardInterface.php new file mode 100644 index 0000000000..bd938ad248 --- /dev/null +++ b/app/Helpers/Csv/WizardInterface.php @@ -0,0 +1,58 @@ +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'); + + } +} \ No newline at end of file diff --git a/app/Http/routes.php b/app/Http/routes.php index 3538bbf428..a391ead7e5 100644 --- a/app/Http/routes.php +++ b/app/Http/routes.php @@ -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 */ diff --git a/app/Providers/FireflyServiceProvider.php b/app/Providers/FireflyServiceProvider.php index 685e9685e9..c528ab0bb6 100644 --- a/app/Providers/FireflyServiceProvider.php +++ b/app/Providers/FireflyServiceProvider.php @@ -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'); diff --git a/app/Support/ExpandedForm.php b/app/Support/ExpandedForm.php index 4bae2bcbbb..62505adac6 100644 --- a/app/Support/ExpandedForm.php +++ b/app/Support/ExpandedForm.php @@ -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 diff --git a/composer.lock b/composer.lock index 74fa0608f8..5d8d22b521 100644 --- a/composer.lock +++ b/composer.lock @@ -882,27 +882,27 @@ }, { "name": "guzzlehttp/guzzle", - "version": "6.0.1", + "version": "6.0.2", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "f992b7b487a816c957d317442bed4966409873e0" + "reference": "a8dfeff00eb84616a17fea7a4d72af35e750410f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/f992b7b487a816c957d317442bed4966409873e0", - "reference": "f992b7b487a816c957d317442bed4966409873e0", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/a8dfeff00eb84616a17fea7a4d72af35e750410f", + "reference": "a8dfeff00eb84616a17fea7a4d72af35e750410f", "shasum": "" }, "require": { - "guzzlehttp/promises": "^1.0.0", - "guzzlehttp/psr7": "^1.0.0", + "guzzlehttp/promises": "~1.0", + "guzzlehttp/psr7": "~1.1", "php": ">=5.5.0" }, "require-dev": { "ext-curl": "*", - "phpunit/phpunit": "^4.0", - "psr/log": "^1.0" + "phpunit/phpunit": "~4.0", + "psr/log": "~1.0" }, "type": "library", "extra": { @@ -912,7 +912,7 @@ }, "autoload": { "files": [ - "src/functions.php" + "src/functions_include.php" ], "psr-4": { "GuzzleHttp\\": "src/" @@ -940,7 +940,7 @@ "rest", "web service" ], - "time": "2015-05-27 16:57:51" + "time": "2015-07-04 20:09:24" }, { "name": "guzzlehttp/promises", @@ -1244,16 +1244,16 @@ }, { "name": "laravel/framework", - "version": "v5.1.5", + "version": "v5.1.6", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "25d872ec1945b30083f3e348039c380307b42d5e" + "reference": "356418f315e4f6bf55d2f4bd451606e45a7595df" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/25d872ec1945b30083f3e348039c380307b42d5e", - "reference": "25d872ec1945b30083f3e348039c380307b42d5e", + "url": "https://api.github.com/repos/laravel/framework/zipball/356418f315e4f6bf55d2f4bd451606e45a7595df", + "reference": "356418f315e4f6bf55d2f4bd451606e45a7595df", "shasum": "" }, "require": { @@ -1368,7 +1368,7 @@ "framework", "laravel" ], - "time": "2015-07-02 02:28:22" + "time": "2015-07-03 13:48:44" }, { "name": "league/commonmark", @@ -2148,16 +2148,16 @@ }, { "name": "phpunit/phpunit-mock-objects", - "version": "2.3.4", + "version": "2.3.5", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "92408bb1968a81b3217a6fdf6c1a198da83caa35" + "reference": "1c330b1b6e1ea8fd15f2fbea46770576e366855c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/92408bb1968a81b3217a6fdf6c1a198da83caa35", - "reference": "92408bb1968a81b3217a6fdf6c1a198da83caa35", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/1c330b1b6e1ea8fd15f2fbea46770576e366855c", + "reference": "1c330b1b6e1ea8fd15f2fbea46770576e366855c", "shasum": "" }, "require": { @@ -2199,7 +2199,7 @@ "mock", "xunit" ], - "time": "2015-06-11 15:55:48" + "time": "2015-07-04 05:41:32" }, { "name": "psr/http-message", @@ -3880,930 +3880,7 @@ "time": "2015-05-25 00:17:51" } ], - "packages-dev": [ - { - "name": "barryvdh/laravel-debugbar", - "version": "v2.0.4", - "source": { - "type": "git", - "url": "https://github.com/barryvdh/laravel-debugbar.git", - "reference": "3edaea0e8056edde00f7d0af13ed0d406412182d" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-debugbar/zipball/3edaea0e8056edde00f7d0af13ed0d406412182d", - "reference": "3edaea0e8056edde00f7d0af13ed0d406412182d", - "shasum": "" - }, - "require": { - "illuminate/support": "~5.0.17|5.1.*", - "maximebf/debugbar": "~1.10.2", - "php": ">=5.4.0", - "symfony/finder": "~2.6" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "psr-4": { - "Barryvdh\\Debugbar\\": "src/" - }, - "files": [ - "src/helpers.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Barry vd. Heuvel", - "email": "barryvdh@gmail.com" - } - ], - "description": "PHP Debugbar integration for Laravel", - "keywords": [ - "debug", - "debugbar", - "laravel", - "profiler", - "webprofiler" - ], - "time": "2015-06-07 07:19:29" - }, - { - "name": "barryvdh/laravel-ide-helper", - "version": "v2.0.6", - "source": { - "type": "git", - "url": "https://github.com/barryvdh/laravel-ide-helper.git", - "reference": "037386153630a7515a1542f29410d8c267651689" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/037386153630a7515a1542f29410d8c267651689", - "reference": "037386153630a7515a1542f29410d8c267651689", - "shasum": "" - }, - "require": { - "illuminate/console": "5.0.x|5.1.x", - "illuminate/filesystem": "5.0.x|5.1.x", - "illuminate/support": "5.0.x|5.1.x", - "php": ">=5.4.0", - "phpdocumentor/reflection-docblock": "2.0.4", - "symfony/class-loader": "~2.3" - }, - "require-dev": { - "doctrine/dbal": "~2.3" - }, - "suggest": { - "doctrine/dbal": "Load information from the database about models for phpdocs (~2.3)" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0-dev" - } - }, - "autoload": { - "psr-4": { - "Barryvdh\\LaravelIdeHelper\\": "src" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Barry vd. Heuvel", - "email": "barryvdh@gmail.com" - } - ], - "description": "Laravel IDE Helper, generates correct PHPDocs for all Facade classes, to improve auto-completion.", - "keywords": [ - "autocomplete", - "codeintel", - "helper", - "ide", - "laravel", - "netbeans", - "phpdoc", - "phpstorm", - "sublime" - ], - "time": "2015-06-25 08:58:59" - }, - { - "name": "codeclimate/php-test-reporter", - "version": "v0.1.2", - "source": { - "type": "git", - "url": "https://github.com/codeclimate/php-test-reporter.git", - "reference": "8ed24ff30f3663ecf40f1c12d6c97eb56c69e646" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/codeclimate/php-test-reporter/zipball/8ed24ff30f3663ecf40f1c12d6c97eb56c69e646", - "reference": "8ed24ff30f3663ecf40f1c12d6c97eb56c69e646", - "shasum": "" - }, - "require": { - "ext-curl": "*", - "php": ">=5.3", - "satooshi/php-coveralls": "0.6.*", - "symfony/console": ">=2.0" - }, - "require-dev": { - "phpunit/phpunit": "3.7.*@stable" - }, - "bin": [ - "composer/bin/test-reporter" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.1.x-dev" - } - }, - "autoload": { - "psr-0": { - "CodeClimate\\Component": "src/", - "CodeClimate\\Bundle": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Code Climate", - "email": "hello@codeclimate.com", - "homepage": "https://codeclimate.com" - } - ], - "description": "PHP client for reporting test coverage to Code Climate", - "homepage": "https://github.com/codeclimate/php-test-reporter", - "keywords": [ - "codeclimate", - "coverage" - ], - "time": "2014-07-23 13:42:41" - }, - { - "name": "fzaninotto/faker", - "version": "v1.4.0", - "source": { - "type": "git", - "url": "https://github.com/fzaninotto/Faker.git", - "reference": "010c7efedd88bf31141a02719f51fb44c732d5a0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/fzaninotto/Faker/zipball/010c7efedd88bf31141a02719f51fb44c732d5a0", - "reference": "010c7efedd88bf31141a02719f51fb44c732d5a0", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "~4.0", - "squizlabs/php_codesniffer": "~1.5" - }, - "type": "library", - "extra": { - "branch-alias": [] - }, - "autoload": { - "psr-0": { - "Faker": "src/", - "Faker\\PHPUnit": "test/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "François Zaninotto" - } - ], - "description": "Faker is a PHP library that generates fake data for you.", - "keywords": [ - "data", - "faker", - "fixtures" - ], - "time": "2014-06-04 14:43:02" - }, - { - "name": "guzzle/guzzle", - "version": "v3.9.3", - "source": { - "type": "git", - "url": "https://github.com/guzzle/guzzle3.git", - "reference": "0645b70d953bc1c067bbc8d5bc53194706b628d9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle3/zipball/0645b70d953bc1c067bbc8d5bc53194706b628d9", - "reference": "0645b70d953bc1c067bbc8d5bc53194706b628d9", - "shasum": "" - }, - "require": { - "ext-curl": "*", - "php": ">=5.3.3", - "symfony/event-dispatcher": "~2.1" - }, - "replace": { - "guzzle/batch": "self.version", - "guzzle/cache": "self.version", - "guzzle/common": "self.version", - "guzzle/http": "self.version", - "guzzle/inflection": "self.version", - "guzzle/iterator": "self.version", - "guzzle/log": "self.version", - "guzzle/parser": "self.version", - "guzzle/plugin": "self.version", - "guzzle/plugin-async": "self.version", - "guzzle/plugin-backoff": "self.version", - "guzzle/plugin-cache": "self.version", - "guzzle/plugin-cookie": "self.version", - "guzzle/plugin-curlauth": "self.version", - "guzzle/plugin-error-response": "self.version", - "guzzle/plugin-history": "self.version", - "guzzle/plugin-log": "self.version", - "guzzle/plugin-md5": "self.version", - "guzzle/plugin-mock": "self.version", - "guzzle/plugin-oauth": "self.version", - "guzzle/service": "self.version", - "guzzle/stream": "self.version" - }, - "require-dev": { - "doctrine/cache": "~1.3", - "monolog/monolog": "~1.0", - "phpunit/phpunit": "3.7.*", - "psr/log": "~1.0", - "symfony/class-loader": "~2.1", - "zendframework/zend-cache": "2.*,<2.3", - "zendframework/zend-log": "2.*,<2.3" - }, - "suggest": { - "guzzlehttp/guzzle": "Guzzle 5 has moved to a new package name. The package you have installed, Guzzle 3, is deprecated." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.9-dev" - } - }, - "autoload": { - "psr-0": { - "Guzzle": "src/", - "Guzzle\\Tests": "tests/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - }, - { - "name": "Guzzle Community", - "homepage": "https://github.com/guzzle/guzzle/contributors" - } - ], - "description": "PHP HTTP client. This library is deprecated in favor of https://packagist.org/packages/guzzlehttp/guzzle", - "homepage": "http://guzzlephp.org/", - "keywords": [ - "client", - "curl", - "framework", - "http", - "http client", - "rest", - "web service" - ], - "time": "2015-03-18 18:23:50" - }, - { - "name": "hamcrest/hamcrest-php", - "version": "v1.2.2", - "source": { - "type": "git", - "url": "https://github.com/hamcrest/hamcrest-php.git", - "reference": "b37020aa976fa52d3de9aa904aa2522dc518f79c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/b37020aa976fa52d3de9aa904aa2522dc518f79c", - "reference": "b37020aa976fa52d3de9aa904aa2522dc518f79c", - "shasum": "" - }, - "require": { - "php": ">=5.3.2" - }, - "replace": { - "cordoval/hamcrest-php": "*", - "davedevelopment/hamcrest-php": "*", - "kodova/hamcrest-php": "*" - }, - "require-dev": { - "phpunit/php-file-iterator": "1.3.3", - "satooshi/php-coveralls": "dev-master" - }, - "type": "library", - "autoload": { - "classmap": [ - "hamcrest" - ], - "files": [ - "hamcrest/Hamcrest.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD" - ], - "description": "This is the PHP port of Hamcrest Matchers", - "keywords": [ - "test" - ], - "time": "2015-05-11 14:41:42" - }, - { - "name": "league/factory-muffin", - "version": "v2.1.1", - "source": { - "type": "git", - "url": "https://github.com/thephpleague/factory-muffin.git", - "reference": "91f0adcdac6b5f7bf2277ac2c90f94352afe65de" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/thephpleague/factory-muffin/zipball/91f0adcdac6b5f7bf2277ac2c90f94352afe65de", - "reference": "91f0adcdac6b5f7bf2277ac2c90f94352afe65de", - "shasum": "" - }, - "require": { - "fzaninotto/faker": "1.4.*", - "php": ">=5.3.3" - }, - "replace": { - "zizaco/factory-muff": "self.version" - }, - "require-dev": { - "illuminate/database": "~4.1", - "phpunit/phpunit": "~4.0" - }, - "suggest": { - "illuminate/database": "Factory Muffin works well with eloquent models." - }, - "type": "library", - "autoload": { - "psr-4": { - "League\\FactoryMuffin\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Graham Campbell", - "email": "graham@mineuk.com" - }, - { - "name": "Zizaco Zizuini", - "email": "zizaco@gmail.com" - }, - { - "name": "Scott Robertson", - "email": "scottymeuk@gmail.com" - } - ], - "description": "The goal of this package is to enable the rapid creation of objects for the purpose of testing.", - "homepage": "http://factory-muffin.thephpleague.com/", - "keywords": [ - "factory", - "laravel", - "testing" - ], - "time": "2014-09-18 18:29:06" - }, - { - "name": "maximebf/debugbar", - "version": "v1.10.4", - "source": { - "type": "git", - "url": "https://github.com/maximebf/php-debugbar.git", - "reference": "7b2006e6e095126b5a061ec33fca3d90ea8a8219" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/maximebf/php-debugbar/zipball/7b2006e6e095126b5a061ec33fca3d90ea8a8219", - "reference": "7b2006e6e095126b5a061ec33fca3d90ea8a8219", - "shasum": "" - }, - "require": { - "php": ">=5.3.0", - "psr/log": "~1.0", - "symfony/var-dumper": "~2.6" - }, - "require-dev": { - "phpunit/phpunit": "~4.0" - }, - "suggest": { - "kriswallsmith/assetic": "The best way to manage assets", - "monolog/monolog": "Log using Monolog", - "predis/predis": "Redis storage" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.10-dev" - } - }, - "autoload": { - "psr-0": { - "DebugBar": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Maxime Bouroumeau-Fuseau", - "email": "maxime.bouroumeau@gmail.com", - "homepage": "http://maximebf.com" - } - ], - "description": "Debug bar in the browser for php application", - "homepage": "https://github.com/maximebf/php-debugbar", - "keywords": [ - "debug" - ], - "time": "2015-02-05 07:51:20" - }, - { - "name": "mockery/mockery", - "version": "0.9.4", - "source": { - "type": "git", - "url": "https://github.com/padraic/mockery.git", - "reference": "70bba85e4aabc9449626651f48b9018ede04f86b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/padraic/mockery/zipball/70bba85e4aabc9449626651f48b9018ede04f86b", - "reference": "70bba85e4aabc9449626651f48b9018ede04f86b", - "shasum": "" - }, - "require": { - "hamcrest/hamcrest-php": "~1.1", - "lib-pcre": ">=7.0", - "php": ">=5.3.2" - }, - "require-dev": { - "phpunit/phpunit": "~4.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.9.x-dev" - } - }, - "autoload": { - "psr-0": { - "Mockery": "library/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Pádraic Brady", - "email": "padraic.brady@gmail.com", - "homepage": "http://blog.astrumfutura.com" - }, - { - "name": "Dave Marshall", - "email": "dave.marshall@atstsolutions.co.uk", - "homepage": "http://davedevelopment.co.uk" - } - ], - "description": "Mockery is a simple yet flexible PHP mock object framework for use in unit testing with PHPUnit, PHPSpec or any other testing framework. Its core goal is to offer a test double framework with a succinct API capable of clearly defining all possible object operations and interactions using a human readable Domain Specific Language (DSL). Designed as a drop in alternative to PHPUnit's phpunit-mock-objects library, Mockery is easy to integrate with PHPUnit and can operate alongside phpunit-mock-objects without the World ending.", - "homepage": "http://github.com/padraic/mockery", - "keywords": [ - "BDD", - "TDD", - "library", - "mock", - "mock objects", - "mockery", - "stub", - "test", - "test double", - "testing" - ], - "time": "2015-04-02 19:54:00" - }, - { - "name": "phpspec/php-diff", - "version": "v1.0.2", - "source": { - "type": "git", - "url": "https://github.com/phpspec/php-diff.git", - "reference": "30e103d19519fe678ae64a60d77884ef3d71b28a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/php-diff/zipball/30e103d19519fe678ae64a60d77884ef3d71b28a", - "reference": "30e103d19519fe678ae64a60d77884ef3d71b28a", - "shasum": "" - }, - "type": "library", - "autoload": { - "psr-0": { - "Diff": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Chris Boulton", - "homepage": "http://github.com/chrisboulton", - "role": "Original developer" - } - ], - "description": "A comprehensive library for generating differences between two hashable objects (strings or arrays).", - "time": "2013-11-01 13:02:21" - }, - { - "name": "phpspec/phpspec", - "version": "2.2.1", - "source": { - "type": "git", - "url": "https://github.com/phpspec/phpspec.git", - "reference": "e9a40577323e67f1de2e214abf32976a0352d8f8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/phpspec/zipball/e9a40577323e67f1de2e214abf32976a0352d8f8", - "reference": "e9a40577323e67f1de2e214abf32976a0352d8f8", - "shasum": "" - }, - "require": { - "doctrine/instantiator": "^1.0.1", - "php": ">=5.3.3", - "phpspec/php-diff": "~1.0.0", - "phpspec/prophecy": "~1.4", - "sebastian/exporter": "~1.0", - "symfony/console": "~2.3", - "symfony/event-dispatcher": "~2.1", - "symfony/finder": "~2.1", - "symfony/process": "~2.1", - "symfony/yaml": "~2.1" - }, - "require-dev": { - "behat/behat": "^3.0.11", - "bossa/phpspec2-expect": "~1.0", - "phpunit/phpunit": "~4.4", - "symfony/filesystem": "~2.1", - "symfony/process": "~2.1" - }, - "suggest": { - "phpspec/nyan-formatters": "~1.0 – Adds Nyan formatters" - }, - "bin": [ - "bin/phpspec" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.2.x-dev" - } - }, - "autoload": { - "psr-0": { - "PhpSpec": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - }, - { - "name": "Marcello Duarte", - "homepage": "http://marcelloduarte.net/" - } - ], - "description": "Specification-oriented BDD framework for PHP 5.3+", - "homepage": "http://phpspec.net/", - "keywords": [ - "BDD", - "SpecBDD", - "TDD", - "spec", - "specification", - "testing", - "tests" - ], - "time": "2015-05-30 15:21:40" - }, - { - "name": "satooshi/php-coveralls", - "version": "v0.6.1", - "source": { - "type": "git", - "url": "https://github.com/satooshi/php-coveralls.git", - "reference": "dd0df95bd37a7cf5c5c50304dfe260ffe4b50760" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/satooshi/php-coveralls/zipball/dd0df95bd37a7cf5c5c50304dfe260ffe4b50760", - "reference": "dd0df95bd37a7cf5c5c50304dfe260ffe4b50760", - "shasum": "" - }, - "require": { - "ext-curl": "*", - "ext-json": "*", - "ext-simplexml": "*", - "guzzle/guzzle": ">=3.0", - "php": ">=5.3", - "psr/log": "1.0.0", - "symfony/config": ">=2.0", - "symfony/console": ">=2.0", - "symfony/stopwatch": ">=2.2", - "symfony/yaml": ">=2.0" - }, - "require-dev": { - "apigen/apigen": "2.8.*@stable", - "pdepend/pdepend": "dev-master", - "phpmd/phpmd": "dev-master", - "phpunit/php-invoker": ">=1.1.0,<1.2.0", - "phpunit/phpunit": "3.7.*@stable", - "sebastian/finder-facade": "dev-master", - "sebastian/phpcpd": "1.4.*@stable", - "squizlabs/php_codesniffer": "1.4.*@stable", - "theseer/fdomdocument": "dev-master" - }, - "bin": [ - "composer/bin/coveralls" - ], - "type": "library", - "autoload": { - "psr-0": { - "Contrib\\Component": "src/", - "Contrib\\Bundle": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Kitamura Satoshi", - "email": "with.no.parachute@gmail.com", - "homepage": "https://www.facebook.com/satooshi.jp" - } - ], - "description": "PHP client library for Coveralls API", - "homepage": "https://github.com/satooshi/php-coveralls", - "keywords": [ - "ci", - "coverage", - "github", - "test" - ], - "time": "2013-05-04 08:07:33" - }, - { - "name": "symfony/class-loader", - "version": "v2.7.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/ClassLoader.git", - "reference": "84843730de48ca0feba91004a03c7c952f8ea1da" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/ClassLoader/zipball/84843730de48ca0feba91004a03c7c952f8ea1da", - "reference": "84843730de48ca0feba91004a03c7c952f8ea1da", - "shasum": "" - }, - "require": { - "php": ">=5.3.9" - }, - "require-dev": { - "symfony/finder": "~2.0,>=2.0.5", - "symfony/phpunit-bridge": "~2.7" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.7-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\ClassLoader\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony ClassLoader Component", - "homepage": "https://symfony.com", - "time": "2015-06-08 09:37:21" - }, - { - "name": "symfony/config", - "version": "v2.7.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/Config.git", - "reference": "58ded81f1f582a87c528ef3dae9a859f78b5f374" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/Config/zipball/58ded81f1f582a87c528ef3dae9a859f78b5f374", - "reference": "58ded81f1f582a87c528ef3dae9a859f78b5f374", - "shasum": "" - }, - "require": { - "php": ">=5.3.9", - "symfony/filesystem": "~2.3" - }, - "require-dev": { - "symfony/phpunit-bridge": "~2.7" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.7-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Config\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Config Component", - "homepage": "https://symfony.com", - "time": "2015-06-11 14:06:56" - }, - { - "name": "symfony/filesystem", - "version": "v2.7.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/Filesystem.git", - "reference": "a0d43eb3e17d4f4c6990289805a488a0482a07f3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/Filesystem/zipball/a0d43eb3e17d4f4c6990289805a488a0482a07f3", - "reference": "a0d43eb3e17d4f4c6990289805a488a0482a07f3", - "shasum": "" - }, - "require": { - "php": ">=5.3.9" - }, - "require-dev": { - "symfony/phpunit-bridge": "~2.7" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.7-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Filesystem\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Filesystem Component", - "homepage": "https://symfony.com", - "time": "2015-06-08 09:37:21" - }, - { - "name": "symfony/stopwatch", - "version": "v2.7.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/Stopwatch.git", - "reference": "c653f1985f6c2b7dbffd04d48b9c0a96aaef814b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/Stopwatch/zipball/c653f1985f6c2b7dbffd04d48b9c0a96aaef814b", - "reference": "c653f1985f6c2b7dbffd04d48b9c0a96aaef814b", - "shasum": "" - }, - "require": { - "php": ">=5.3.9" - }, - "require-dev": { - "symfony/phpunit-bridge": "~2.7" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.7-dev" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Component\\Stopwatch\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony Stopwatch Component", - "homepage": "https://symfony.com", - "time": "2015-06-04 20:11:48" - } - ], + "packages-dev": null, "aliases": [], "minimum-stability": "stable", "stability-flags": { diff --git a/config/csv.php b/config/csv.php new file mode 100644 index 0000000000..e141c2f438 --- /dev/null +++ b/config/csv.php @@ -0,0 +1,139 @@ + [ + '_ignore' => [ + 'name' => '(ignore this column)', + 'mappable' => false, + 'converter' => 'Ignore', + 'field' => 'ignored', + ], + 'bill-id' => [ + 'name' => 'Bill ID (matching Firefly)', + 'mappable' => true, + ], + 'bill-name' => [ + 'name' => 'Bill name', + 'mappable' => true, + ], + 'currency-id' => [ + 'name' => 'Currency ID (matching Firefly)', + 'mappable' => true, + ], + 'currency-name' => [ + 'name' => 'Currency name (matching Firefly)', + 'mappable' => true, + ], + 'currency-code' => [ + 'name' => 'Currency code (ISO 4217)', + 'mappable' => true, + 'converter' => 'CurrencyCode', + 'field' => 'currency', + 'mapper' => 'TransactionCurrency' + ], + 'currency-symbol' => [ + 'name' => 'Currency symbol (matching Firefly)', + 'mappable' => true, + ], + 'description' => [ + 'name' => 'Description', + 'mappable' => false, + 'converter' => 'Description', + 'field' => 'description', + ], + 'date-transaction' => [ + 'name' => 'Date', + 'mappable' => false, + 'converter' => 'Date', + 'field' => 'date', + ], + 'date-rent' => [ + 'name' => 'Rent calculation date', + 'mappable' => false, + 'converter' => 'Date', + 'field' => 'date-rent', + ], + 'budget-id' => [ + 'name' => 'Budget ID (matching Firefly)', + 'mappable' => true, + ], + 'budget-name' => [ + 'name' => 'Budget name', + 'mappable' => true, + ], + 'rabo-debet-credit' => [ + 'name' => 'Rabobank specific debet/credit indicator', + 'mappable' => false, + 'converter' => 'RabobankDebetCredit', + 'field' => 'amount-modifier', + ], + 'category-id' => [ + 'name' => 'Category ID (matching Firefly)', + 'mappable' => true, + ], + 'category-name' => [ + 'name' => 'Category name', + 'mappable' => true, + ], + 'tags-comma' => [ + 'name' => 'Tags (comma separated)', + 'mappable' => true, + ], + 'tags-space' => [ + 'name' => 'Tags (space separated)', + 'mappable' => true, + ], + 'account-id' => [ + 'name' => 'Asset account ID (matching Firefly)', + 'mappable' => true, + ], + 'account-name' => [ + 'name' => 'Asset account name', + 'mappable' => true, + ], + 'account-iban' => [ + 'name' => 'Asset account IBAN', + 'mappable' => true, + 'converter' => 'AccountIban', + 'field' => 'asset-account', + 'mapper' => 'AssetAccount' + ], + 'opposing-id' => [ + 'name' => 'Expense or revenue account ID (matching Firefly)', + 'mappable' => true, + ], + 'opposing-name' => [ + 'name' => 'Expense or revenue account name', + 'mappable' => true, + 'converter' => 'OpposingName', + 'field' => 'opposing-account' + ], + 'opposing-iban' => [ + 'name' => 'Expense or revenue account IBAN', + 'mappable' => true, + ], + 'amount' => [ + 'name' => 'Amount', + 'mappable' => false, + 'converter' => 'Amount', + 'field' => 'amount', + ], + 'sepa-ct-id' => [ + 'name' => 'SEPA Credit Transfer end-to-end ID', + 'mappable' => false, + 'converter' => 'Description', + 'field' => 'description', + ], + 'sepa-ct-op' => [ + 'name' => 'SEPA Credit Transfer opposing account', + 'mappable' => false, + 'converter' => 'Description', + 'field' => 'description', + ], + 'sepa-db' => [ + 'name' => 'SEPA Direct Debet', + 'mappable' => false, + 'converter' => 'Description', + 'field' => 'description', + ], + ] +]; \ No newline at end of file diff --git a/config/firefly.php b/config/firefly.php index e56f64e13e..81f741bd77 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -5,6 +5,7 @@ return [ 'version' => '3.4.6', 'index_periods' => ['1D', '1W', '1M', '3M', '6M', '1Y', 'custom'], 'budget_periods' => ['daily', 'weekly', 'monthly', 'quarterly', 'half-year', 'yearly'], + 'csv_import_enabled' => false, 'piggy_bank_periods' => [ 'week' => 'Week', 'month' => 'Month', diff --git a/config/twigbridge.php b/config/twigbridge.php index 04d882d65e..8d5621c253 100644 --- a/config/twigbridge.php +++ b/config/twigbridge.php @@ -145,12 +145,12 @@ return [ 'ExpandedForm' => [ 'is_safe' => [ 'date', 'text', 'select', 'balance', 'optionsList', 'checkbox', 'amount', 'tags', 'integer', 'textarea', 'location', - 'multiRadio' + 'multiRadio','file' ] ], 'Form' => [ 'is_safe' => [ - 'input', 'select', 'checkbox', 'model', 'open', 'radio', 'textarea' + 'input', 'select', 'checkbox', 'model', 'open', 'radio', 'textarea','file' ] ], ], diff --git a/database/seeds/TestDataSeeder.php b/database/seeds/TestDataSeeder.php index fedf19c97a..b187830979 100644 --- a/database/seeds/TestDataSeeder.php +++ b/database/seeds/TestDataSeeder.php @@ -98,7 +98,7 @@ class TestDataSeeder extends Seeder protected function createAssetAccounts() { - $assets = ['MyBank Checking Account', 'Savings', 'Shared', 'Creditcard']; + $assets = ['MyBank Checking Account', 'Savings', 'Shared', 'Creditcard', 'Emergencies', 'STE']; $assetMeta = [ [ 'accountRole' => 'defaultAsset', @@ -114,6 +114,12 @@ class TestDataSeeder extends Seeder 'ccMonthlyPaymentDate' => '2015-05-27', 'ccType' => 'monthlyFull' ], + [ + 'accountRole' => 'savingAsset', + ], + [ + 'accountRole' => 'savingAsset', + ], ]; diff --git a/resources/lang/en/firefly.php b/resources/lang/en/firefly.php index d6feb0f661..1374379ef4 100644 --- a/resources/lang/en/firefly.php +++ b/resources/lang/en/firefly.php @@ -19,6 +19,57 @@ return [ 'never' => 'Never', 'search_results_for' => 'Search results for ":query"', + // csv import: + 'csv_import' => 'Import CSV file', + 'csv' => 'CSV', + 'csv_index_title' => 'Upload and import a CSV file', + 'csv_index_text' => + 'This form allows you to import a CSV file with transactions into Firefly. It is based on the excellent CSV importer made by' . + ' the folks at Atlassian. Simply upload your CSV file and follow the instructions.', + 'csv_index_beta_warning' => 'This tool is very much in beta. Please proceed with caution', + 'csv_header_help' => 'Check this box when your CSV file\'s first row consists of column names, not actual data', + 'csv_date_help' => 'Date time format in your CSV. Follow the format like this' . + ' page indicates. The default value will parse dates that look like this: ' . date('Ymd'), + 'csv_csv_file_help' => 'Select the CSV file here. You can only upload one file at a time', + 'csv_csv_config_file_help' => 'Select your CSV import configuration here. If you do not know what this is, ignore it. It will be explained later.', + 'csv_upload_button' => 'Start importing CSV', + 'csv_column_roles_title' => 'Define column roles', + 'csv_column_roles_text' => 'Firefly does not know what each column means. You need to indicate what every column is. Please check out the example ' + . 'data if you\'re not sure yourself. Click on the question mark (top right of the page) to learn what' + . ' each column means. If you want to map imported data onto existing data in Firefly, use the checkbox. ' + . 'The next step will show you what this button does.', + 'csv_column_roles_table' => 'Column roles', + 'csv_column' => 'CSV column', + 'cvs_column_name' => 'CSV column name', + 'cvs_column_example' => 'Column example data', + 'cvs_column_role' => 'Column contains?', + 'csv_do_map_value' => 'Map value?', + 'csv_continue' => 'Continue to the next step', + 'csv_go_back' => 'Go back to the previous step', + 'csv_map_title' => 'Map found values to existing values', + 'csv_map_text' => + 'This page allows you to map the values from the CSV file to existing entries in your database. This ensures that accounts and other' + . ' things won\'t be created twice.', + 'cvs_field_value' => 'Field value from CSV', + 'csv_field_mapped_to' => 'Must be mapped to...', + 'csv_download_config_title' => 'Download CSV configuration', + 'csv_download_config_text' => 'Everything you\'ve just set up can be downloaded as a configuration file. Click the button to do so.', + 'csv_more_information_text' => 'If the import fails, you can use this configuration file so you don\'t have to start all over again.', + 'csv_do_download_config' => 'Download configuration file.', + 'csv_empty_description' => '(empty description)', + 'csv_upload_form' => 'CSV upload form', + 'csv_index_unsupported_warning' => 'The CSV importer is yet incapable of doing the following:', + 'csv_unsupported_map' => 'The importer cannot map the column ":columnRole" to existing values in the database.', + 'csv_unsupported_value' => 'The importer does not know how to handle values in columns marked as ":columnRole".', + // 'csv_index_text' => 'Here be explanation.', + // 'csv_upload_form' => 'Upload form', + // 'upload_csv_file' => 'Upload CSV file', + // 'csv_header_help' => 'Check this when bla bla', + // 'csv_date_help' => + // 'csv_row' => 'row', + 'csv_upload_not_writeable' => 'Cannot write to the path mentioned here. Cannot upload', + // create new stuff: 'create_new_withdrawal' => 'Create new withdrawal', 'create_new_deposit' => 'Create new deposit', diff --git a/resources/lang/en/form.php b/resources/lang/en/form.php index 4edfa130f8..8cb9937341 100644 --- a/resources/lang/en/form.php +++ b/resources/lang/en/form.php @@ -46,6 +46,10 @@ return [ 'symbol' => 'Symbol', 'code' => 'Code', 'iban' => 'IBAN', + 'csv' => 'CSV file', + 'has_headers' => 'Headers', + 'date_format' => 'Date format', + 'csv_config' => 'CSV import configuration', 'store_new_withdrawal' => 'Store new withdrawal', 'store_new_deposit' => 'Store new deposit', diff --git a/resources/lang/nl/firefly.php b/resources/lang/nl/firefly.php index bdab21f817..aaa2a9a90e 100644 --- a/resources/lang/nl/firefly.php +++ b/resources/lang/nl/firefly.php @@ -19,6 +19,16 @@ return [ 'never' => 'Nooit', 'search_results_for' => 'Zoekresultaten voor ":query"', + // csv import: + 'csv_import' => 'Importeer CSV-bestand', + 'csv' => 'CSV', + 'csv_index_text' => 'Hier komt uitleg.', + 'csv_upload_form' => 'Upload formulier', + 'upload_csv_file' => 'Upload CSV-bestand', + 'csv_header_help' => 'Check dit als bla bla', + 'csv_row' => 'rij', + 'upload_not_writeable' => 'Cannot write to the path mentioned here. Cannot upload', + // create new stuff: 'create_new_withdrawal' => 'Nieuwe uitgave', 'create_new_deposit' => 'Nieuwe inkomsten', diff --git a/resources/lang/nl/form.php b/resources/lang/nl/form.php index f2ce23701f..397c87cb76 100644 --- a/resources/lang/nl/form.php +++ b/resources/lang/nl/form.php @@ -46,6 +46,9 @@ return [ 'symbol' => 'Symbool', 'code' => 'Code', 'iban' => 'IBAN', + 'csv' => 'CSV-bestand', + 'has_headers' => 'Eerste rij zijn kolomnamen', + 'date_format' => 'Datumformaat', 'store_new_withdrawal' => 'Nieuwe uitgave opslaan', 'store_new_deposit' => 'Nieuwe inkomsten opslaan', diff --git a/resources/twig/csv/column-roles.twig b/resources/twig/csv/column-roles.twig new file mode 100644 index 0000000000..3480bdbd0f --- /dev/null +++ b/resources/twig/csv/column-roles.twig @@ -0,0 +1,87 @@ +{% extends "./layout/default.twig" %} + +{% block breadcrumbs %} + {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName) }} +{% endblock %} + +{% block content %} + + +
+
+
+
+

{{ 'csv_column_roles_title'|_ }}

+ + +
+ +
+ +
+
+

{{ 'csv_column_roles_text'|_ }}

+
+
+ +
+
+
+ + +
+
+
+
+

{{ 'csv_column_roles_table'|_ }}

+ + +
+ +
+ +
+
+ + + + + + + + + {% for index,header in headers %} + + + + + + + + {% endfor %} +
{{ 'cvs_column_name'|_ }}{{ 'cvs_column_example'|_ }}{{ 'cvs_column_role'|_ }}{{ 'csv_do_map_value'|_ }}
{{ header }}{{ example[index] }} + {{ Form.select(('role['~index~']'), availableRoles,roles[index]) }} + + {{ Form.checkbox(('map['~index~']'),1,map[index]) }} +
+ + +
+
+
+
+ +
+
+
+
+ {{ 'csv_go_back'|_ }} + +
+
+
+
+
+{% endblock %} diff --git a/resources/twig/csv/download-config.twig b/resources/twig/csv/download-config.twig new file mode 100644 index 0000000000..302670614d --- /dev/null +++ b/resources/twig/csv/download-config.twig @@ -0,0 +1,50 @@ +{% extends "./layout/default.twig" %} + +{% block breadcrumbs %} + {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName) }} +{% endblock %} + +{% block content %} + + +
+
+
+
+

{{ 'csv_download_config_title'|_ }}

+ + +
+ +
+ +
+
+

+ {{ 'csv_download_config_text'|_ }} +

+ +

+ {{ 'csv_do_download_config'|_ }} +

+ +

+ {{ 'csv_more_information_text'|_ }} +

+
+
+
+
+ +
+
+ +
+
+ +{% endblock %} diff --git a/resources/twig/csv/index.twig b/resources/twig/csv/index.twig new file mode 100644 index 0000000000..10f00c9cf2 --- /dev/null +++ b/resources/twig/csv/index.twig @@ -0,0 +1,100 @@ +{% extends "./layout/default.twig" %} + +{% block breadcrumbs %} + {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName) }} +{% endblock %} + +{% block content %} + + +
+
+
+
+

{{ 'csv_index_title'|_ }}

+ + +
+ +
+ +
+
+ {{ 'csv_index_text'|_ }} +

{{ 'csv_index_beta_warning'|_ }}

+ {% if unsupported|length > 0 %} +

{{ 'csv_index_unsupported_warning'|_ }}

+
    + {% for message in unsupported %} +
  • {{ message }}
  • + {% endfor %} +
+ {% endif %} +
+
+ +
+
+ +
+ + +
+
+
+
+

{{ 'csv_upload_form'|_ }}

+ + +
+ +
+ +
+
+ + + {{ ExpandedForm.checkbox('has_headers',false,null,{helpText: 'csv_header_help'|_}) }} + {{ ExpandedForm.text('date_format','Ymd',{helpText: 'csv_date_help'|_}) }} + + {{ ExpandedForm.file('csv',{helpText: 'csv_csv_file_help'|_}) }} + + {{ ExpandedForm.file('csv_config',{helpText: 'csv_csv_config_file_help'|_}) }} + + {% if not uploadPossible %} +
+
+   +
+ +
+
{{ path }}
+

+ {{ 'csv_upload_not_writeable'|_ }} +

+
+
+ {% endif %} + +
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+ + + + +{% endblock %} diff --git a/resources/twig/csv/map.twig b/resources/twig/csv/map.twig new file mode 100644 index 0000000000..97ce18b582 --- /dev/null +++ b/resources/twig/csv/map.twig @@ -0,0 +1,92 @@ +{% extends "./layout/default.twig" %} + +{% block breadcrumbs %} + {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName) }} +{% endblock %} + +{% block content %} + + +
+
+
+
+

{{ 'csv_map_title'|_ }}

+ + +
+ +
+ +
+
+

+ {{ 'csv_map_text'|_ }} +

+
+
+ +
+
+
+ + + {% for index,columnName in map %} + +
+
+
+
+

{{ Config.get('csv.roles.'~columnName~'.name') }}

+ + +
+ +
+ +
+
+ + + + + + + {% for value in values[index] %} + + + + + {% endfor %} + + + +
{{ 'cvs_field_value'|_ }}{{ 'csv_field_mapped_to'|_ }}
{{ value }} + {{ Form.select('mapping['~index~']['~value~']',options[index], mapped[index][value]) }} +
+ + +
+
+
+
+ {% endfor %} + + +
+
+
+
+ {{ 'csv_go_back'|_ }} + +
+
+
+
+ +
+ + +{% endblock %} diff --git a/resources/twig/error.twig b/resources/twig/error.twig index e0621f17d8..4390f9c05c 100644 --- a/resources/twig/error.twig +++ b/resources/twig/error.twig @@ -4,9 +4,7 @@
-

Firefly
- Error -

+

Sorry, an error occurred.

diff --git a/resources/twig/form/checkbox.twig b/resources/twig/form/checkbox.twig index bea165b856..b2bc258cd0 100644 --- a/resources/twig/form/checkbox.twig +++ b/resources/twig/form/checkbox.twig @@ -7,6 +7,7 @@ {{ Form.checkbox(name, value, options.checked, options) }} + {% include 'form/help.twig' %} {% include 'form/feedback.twig' %} diff --git a/resources/twig/form/file.twig b/resources/twig/form/file.twig new file mode 100644 index 0000000000..0596ef7dbd --- /dev/null +++ b/resources/twig/form/file.twig @@ -0,0 +1,9 @@ +
+ + +
+ {{ Form.file(name, options) }} + {% include 'form/help.twig' %} + {% include 'form/feedback.twig' %} +
+
diff --git a/resources/twig/form/help.twig b/resources/twig/form/help.twig index e1e2d011ea..d402fb623b 100644 --- a/resources/twig/form/help.twig +++ b/resources/twig/form/help.twig @@ -1,3 +1,3 @@ {% if options.helpText %} -

{{ options.helpText }}

+

{{ options.helpText|raw }}

{% endif %} diff --git a/resources/twig/form/text.twig b/resources/twig/form/text.twig index 0b199f9117..c2f890c0fe 100644 --- a/resources/twig/form/text.twig +++ b/resources/twig/form/text.twig @@ -3,6 +3,7 @@
{{ Form.input('text', name, value, options) }} + {% include 'form/help.twig' %} {% include 'form/feedback.twig' %}
diff --git a/resources/twig/partials/menu-sidebar.twig b/resources/twig/partials/menu-sidebar.twig index ba080666f2..c3bfafa52c 100644 --- a/resources/twig/partials/menu-sidebar.twig +++ b/resources/twig/partials/menu-sidebar.twig @@ -123,6 +123,11 @@
  • {{ 'currencies'|_ }}
  • + {% if Config.get('firefly.csv_import_enabled') %} +
  • + {{ 'csv_import'|_ }} +
  • + {% endif %} diff --git a/storage/upload/.gitignore b/storage/upload/.gitignore new file mode 100644 index 0000000000..d6b7ef32c8 --- /dev/null +++ b/storage/upload/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore