From 86c22c9fdd74689f88cc74cd58b5891f7d3adccb Mon Sep 17 00:00:00 2001 From: James Cole Date: Thu, 4 Feb 2016 17:16:16 +0100 Subject: [PATCH] New export functionality. --- app/Export/Collector/AttachmentCollector.php | 54 ++++++ app/Export/Collector/BasicCollector.php | 55 +++++++ app/Export/Collector/CollectorInterface.php | 21 +++ app/Export/Collector/UploadCollector.php | 60 +++++++ app/Export/ConfigurationFile.php | 55 +++++++ app/Export/Entry.php | 120 ++++++++++++++ app/Export/Exporter/BasicExporter.php | 55 +++++++ app/Export/Exporter/CsvExporter.php | 80 +++++++++ app/Export/Exporter/ExporterInterface.php | 41 +++++ app/Export/JournalCollector.php | 65 ++++++++ app/Export/Processor.php | 154 ++++++++++++++++++ app/Http/Requests/ExportFormRequest.php | 52 ++++++ app/Providers/FireflyServiceProvider.php | 2 +- .../ExportJob/ExportJobRepository.php | 66 ++++++++ .../ExportJobRepositoryInterface.php | 38 +++++ app/Support/Migration/TestData.php | 82 ++++++++++ app/Validation/FireflyValidator.php | 4 +- config/firefly.php | 20 ++- public/images/loading-wide.gif | Bin 0 -> 12274 bytes public/js/export/index.js | 121 ++++++++++++++ resources/lang/en_US/firefly.php | 12 ++ resources/lang/en_US/form.php | 6 + resources/views/export/index.twig | 129 +++++++++++++++ resources/views/partials/menu-sidebar.twig | 32 +++- 24 files changed, 1311 insertions(+), 13 deletions(-) create mode 100644 app/Export/Collector/AttachmentCollector.php create mode 100644 app/Export/Collector/BasicCollector.php create mode 100644 app/Export/Collector/CollectorInterface.php create mode 100644 app/Export/Collector/UploadCollector.php create mode 100644 app/Export/ConfigurationFile.php create mode 100644 app/Export/Entry.php create mode 100644 app/Export/Exporter/BasicExporter.php create mode 100644 app/Export/Exporter/CsvExporter.php create mode 100644 app/Export/Exporter/ExporterInterface.php create mode 100644 app/Export/JournalCollector.php create mode 100644 app/Export/Processor.php create mode 100644 app/Http/Requests/ExportFormRequest.php create mode 100644 app/Repositories/ExportJob/ExportJobRepository.php create mode 100644 app/Repositories/ExportJob/ExportJobRepositoryInterface.php create mode 100644 public/images/loading-wide.gif create mode 100644 public/js/export/index.js create mode 100644 resources/views/export/index.twig diff --git a/app/Export/Collector/AttachmentCollector.php b/app/Export/Collector/AttachmentCollector.php new file mode 100644 index 0000000000..93bc425909 --- /dev/null +++ b/app/Export/Collector/AttachmentCollector.php @@ -0,0 +1,54 @@ +attachments()->get(); + + Log::debug('Found ' . $attachments->count() . ' attachments.'); + + /** @var Attachment $attachment */ + foreach ($attachments as $attachment) { + $originalFile = storage_path('upload') . DIRECTORY_SEPARATOR . 'at-' . $attachment->id . '.data'; + if (file_exists($originalFile)) { + Log::debug('Stored 1 attachment'); + $decrypted = Crypt::decrypt(file_get_contents($originalFile)); + $newFile = storage_path('export') . DIRECTORY_SEPARATOR . $this->job->key . '-Attachment nr. ' . $attachment->id . ' - ' . $attachment->filename; + file_put_contents($newFile, $decrypted); + $this->getFiles()->push($newFile); + } + } + } +} \ No newline at end of file diff --git a/app/Export/Collector/BasicCollector.php b/app/Export/Collector/BasicCollector.php new file mode 100644 index 0000000000..b6b98cf397 --- /dev/null +++ b/app/Export/Collector/BasicCollector.php @@ -0,0 +1,55 @@ +files = new Collection; + $this->job = $job; + } + + /** + * @return Collection + */ + public function getFiles() + { + return $this->files; + } + + /** + * @param Collection $files + */ + public function setFiles($files) + { + $this->files = $files; + } + + +} \ No newline at end of file diff --git a/app/Export/Collector/CollectorInterface.php b/app/Export/Collector/CollectorInterface.php new file mode 100644 index 0000000000..313816d4a2 --- /dev/null +++ b/app/Export/Collector/CollectorInterface.php @@ -0,0 +1,21 @@ +id . '-'; + $len = strlen($expected); + foreach ($files as $entry) { + if (substr($entry, 0, $len) === $expected) { + // this is an original upload. + $parts = explode('-', str_replace(['.csv.encrypted', $expected], '', $entry)); + $originalUpload = intval($parts[1]); + $date = date('Y-m-d \a\t H-i-s', $originalUpload); + $newFileName = 'Old CSV import dated ' . $date . '.csv'; + $content = Crypt::decrypt(file_get_contents($path . DIRECTORY_SEPARATOR . $entry)); + $fullPath = storage_path('export') . DIRECTORY_SEPARATOR . $this->job->key . '-' . $newFileName; + + // write to file: + file_put_contents($fullPath, $content); + + // add entry to set: + $this->getFiles()->push($fullPath); + } + } + } +} \ No newline at end of file diff --git a/app/Export/ConfigurationFile.php b/app/Export/ConfigurationFile.php new file mode 100644 index 0000000000..db86402d66 --- /dev/null +++ b/app/Export/ConfigurationFile.php @@ -0,0 +1,55 @@ +job = $job; + } + + /** + * @return bool + */ + public function make() + { + $fields = array_keys(get_class_vars(Entry::class)); + $types = Entry::getTypes(); + + $configuration = [ + 'date-format' => 'Y-m-d', // unfortunately, this is hard-coded. + 'has-headers' => true, + 'map' => [], // we could build a map if necessary for easy re-import. + 'roles' => [], + 'mapped' => [], + 'specifix' => [], + ]; + foreach ($fields as $field) { + $configuration['roles'][] = $types[$field]; + } + + $file = storage_path('export') . DIRECTORY_SEPARATOR . $this->job->key . '-configuration.json'; + file_put_contents($file, json_encode($configuration, JSON_PRETTY_PRINT)); + + return $file; + } + +} \ No newline at end of file diff --git a/app/Export/Entry.php b/app/Export/Entry.php new file mode 100644 index 0000000000..559327871a --- /dev/null +++ b/app/Export/Entry.php @@ -0,0 +1,120 @@ +setDescription($journal->description); + $entry->setDate($journal->date->format('Y-m-d')); + $entry->setAmount($journal->amount); + + return $entry; + + } + + /** + * @return array + */ + public static function getTypes() + { + // key = field name (see top of class) + // value = field type (see csv.php under 'roles') + return [ + 'amount' => 'amount', + 'date' => 'date-transaction', + 'description' => 'description', + ]; + } + + /** + * @return string + */ + public function getAmount() + { + return $this->amount; + } + + /** + * @param string $amount + */ + public function setAmount($amount) + { + $this->amount = $amount; + } + + /** + * @return string + */ + public function getDate() + { + return $this->date; + } + + /** + * @param string $date + */ + public function setDate($date) + { + $this->date = $date; + } + + /** + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * @param string $description + */ + public function setDescription($description) + { + $this->description = $description; + } + + +} \ No newline at end of file diff --git a/app/Export/Exporter/BasicExporter.php b/app/Export/Exporter/BasicExporter.php new file mode 100644 index 0000000000..aaa8ea1dad --- /dev/null +++ b/app/Export/Exporter/BasicExporter.php @@ -0,0 +1,55 @@ +entries = new Collection; + $this->job = $job; + } + + /** + * @return Collection + */ + public function getEntries() + { + return $this->entries; + } + + /** + * @param Collection $entries + */ + public function setEntries(Collection $entries) + { + $this->entries = $entries; + } + + + +} \ No newline at end of file diff --git a/app/Export/Exporter/CsvExporter.php b/app/Export/Exporter/CsvExporter.php new file mode 100644 index 0000000000..906667a5f2 --- /dev/null +++ b/app/Export/Exporter/CsvExporter.php @@ -0,0 +1,80 @@ +fileName; + } + + /** + * + */ + public function run() + { + // create temporary file: + $this->tempFile(); + + // create CSV writer: + $writer = Writer::createFromPath(new SplFileObject($this->fileName, 'a+'), 'w'); + //the $writer object open mode will be 'w'!! + + // all rows: + $rows = []; + + // add header: + $first = $this->getEntries()->first(); + $rows[] = array_keys(get_object_vars($first)); + + // then the rest: + /** @var Entry $entry */ + foreach ($this->getEntries() as $entry) { + $rows[] = array_values(get_object_vars($entry)); + + } + $writer->insertAll($rows); + } + + private function tempFile() + { + $fileName = $this->job->key . '-records.csv'; + $this->fileName = storage_path('export') . DIRECTORY_SEPARATOR . $fileName; + $this->handler = fopen($this->fileName, 'w'); + } +} \ No newline at end of file diff --git a/app/Export/Exporter/ExporterInterface.php b/app/Export/Exporter/ExporterInterface.php new file mode 100644 index 0000000000..b8f3431a51 --- /dev/null +++ b/app/Export/Exporter/ExporterInterface.php @@ -0,0 +1,41 @@ +accounts = $accounts; + $this->user = $user; + $this->start = $start; + $this->end = $end; + } + + /** + * @return Collection + */ + public function collect() + { + // get all the journals: + $ids = $this->accounts->pluck('id')->toArray(); + + return $this->user->transactionjournals() + ->leftJoin('transactions', 'transactions.transaction_journal_id', '=', 'transaction_journals.id') + ->whereIn('transactions.account_id', $ids) + ->before($this->end) + ->after($this->start) + ->orderBy('transaction_journals.date') + ->get(['transaction_journals.*']); + } + +} \ No newline at end of file diff --git a/app/Export/Processor.php b/app/Export/Processor.php new file mode 100644 index 0000000000..6f43561dd2 --- /dev/null +++ b/app/Export/Processor.php @@ -0,0 +1,154 @@ +settings = $settings; + $this->accounts = $settings['accounts']; + $this->exportFormat = $settings['exportFormat']; + $this->includeAttachments = $settings['includeAttachments']; + $this->includeConfig = $settings['includeConfig']; + $this->includeOldUploads = $settings['includeOldUploads']; + $this->job = $settings['job']; + $this->journals = new Collection; + $this->exportEntries = new Collection; + $this->files = new Collection; + + } + + /** + * + */ + public function collectAttachments() + { + $attachmentCollector = app('FireflyIII\Export\Collector\AttachmentCollector', [$this->job]); + $attachmentCollector->run(); + $this->files = $this->files->merge($attachmentCollector->getFiles()); + } + + /** + * + */ + public function collectJournals() + { + $args = [$this->accounts, Auth::user(), $this->settings['startDate'], $this->settings['endDate']]; + $journalCollector = app('FireflyIII\Export\JournalCollector', $args); + $this->journals = $journalCollector->collect(); + } + + public function collectOldUploads() + { + $uploadCollector = app('FireflyIII\Export\Collector\UploadCollector', [$this->job]); + $uploadCollector->run(); + + $this->files = $this->files->merge($uploadCollector->getFiles()); + } + + /** + * + */ + public function convertJournals() + { + /** @var TransactionJournal $journal */ + foreach ($this->journals as $journal) { + $this->exportEntries->push(Entry::fromJournal($journal)); + } + } + + public function createConfigFile() + { + $this->configurationMaker = app('FireflyIII\Export\ConfigurationFile', [$this->job]); + $this->files->push($this->configurationMaker->make()); + } + + public function createZipFile() + { + $zip = new ZipArchive; + $filename = storage_path('export') . DIRECTORY_SEPARATOR . $this->job->key . '.zip'; + + if ($zip->open($filename, ZipArchive::CREATE) !== true) { + throw new FireflyException('Cannot store zip file.'); + } + // for each file in the collection, add it to the zip file. + $search = storage_path('export') . DIRECTORY_SEPARATOR . $this->job->key . '-'; + /** @var string $file */ + foreach ($this->getFiles() as $file) { + $zipName = str_replace($search, '', $file); + $zip->addFile($file, $zipName); + } + $zip->close(); + } + + /** + * + */ + public function exportJournals() + { + $exporterClass = Config::get('firefly.export_formats.' . $this->exportFormat); + $exporter = app($exporterClass, [$this->job]); + $exporter->setEntries($this->exportEntries); + $exporter->run(); + $this->files->push($exporter->getFileName()); + } + + /** + * @return Collection + */ + public function getFiles() + { + return $this->files; + } +} \ No newline at end of file diff --git a/app/Http/Requests/ExportFormRequest.php b/app/Http/Requests/ExportFormRequest.php new file mode 100644 index 0000000000..979d63cbbb --- /dev/null +++ b/app/Http/Requests/ExportFormRequest.php @@ -0,0 +1,52 @@ +subDay()->format('Y-m-d'); + $today = Carbon::create()->addDay()->format('Y-m-d'); + $formats = join(',', array_keys(config('firefly.export_formats'))); + + return [ + 'start_date' => 'required|date|after:' . $first, + 'end_date' => 'required|date|before:' . $today, + 'accounts' => 'required', + 'job' => 'required|belongsToUser:export_jobs,key', + 'accounts.*' => 'required|exists:accounts,id|belongsToUser:accounts', + 'include_attachments' => 'in:0,1', + 'include_config' => 'in:0,1', + 'exportFormat' => 'in:' . $formats, + ]; + } +} diff --git a/app/Providers/FireflyServiceProvider.php b/app/Providers/FireflyServiceProvider.php index 13712858c3..46f5160c57 100644 --- a/app/Providers/FireflyServiceProvider.php +++ b/app/Providers/FireflyServiceProvider.php @@ -93,6 +93,7 @@ class FireflyServiceProvider extends ServiceProvider $this->app->bind('FireflyIII\Repositories\Attachment\AttachmentRepositoryInterface', 'FireflyIII\Repositories\Attachment\AttachmentRepository'); $this->app->bind('FireflyIII\Repositories\Rule\RuleRepositoryInterface', 'FireflyIII\Repositories\Rule\RuleRepository'); $this->app->bind('FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface', 'FireflyIII\Repositories\RuleGroup\RuleGroupRepository'); + $this->app->bind('FireflyIII\Repositories\ExportJob\ExportJobRepositoryInterface', 'FireflyIII\Repositories\ExportJob\ExportJobRepository'); $this->app->bind('FireflyIII\Support\Search\SearchInterface', 'FireflyIII\Support\Search\Search'); // CSV import @@ -126,7 +127,6 @@ class FireflyServiceProvider extends ServiceProvider $this->app->bind('FireflyIII\Helpers\Report\BalanceReportHelperInterface', 'FireflyIII\Helpers\Report\BalanceReportHelper'); $this->app->bind('FireflyIII\Helpers\Report\BudgetReportHelperInterface', 'FireflyIII\Helpers\Report\BudgetReportHelper'); - } } diff --git a/app/Repositories/ExportJob/ExportJobRepository.php b/app/Repositories/ExportJob/ExportJobRepository.php new file mode 100644 index 0000000000..8125c61033 --- /dev/null +++ b/app/Repositories/ExportJob/ExportJobRepository.php @@ -0,0 +1,66 @@ +subDay(); + ExportJob::where('created_at', '<', $dayAgo->format('Y-m-d H:i:s')) + ->where('status', 'never_started') + // TODO also delete others. + ->delete(); + + return true; + } + + /** + * @return ExportJob + */ + public function create() + { + $exportJob = new ExportJob; + $exportJob->user()->associate(Auth::user()); + /* + * In theory this random string could give db error. + */ + $exportJob->key = Str::random(12); + $exportJob->status = 'export_status_never_started'; + $exportJob->save(); + + return $exportJob; + } + + /** + * @param $key + * + * @return ExportJob|null + */ + public function findByKey($key) + { + return Auth::user()->exportJobs()->where('key', $key)->first(); + } + +} \ No newline at end of file diff --git a/app/Repositories/ExportJob/ExportJobRepositoryInterface.php b/app/Repositories/ExportJob/ExportJobRepositoryInterface.php new file mode 100644 index 0000000000..68265b2bc5 --- /dev/null +++ b/app/Repositories/ExportJob/ExportJobRepositoryInterface.php @@ -0,0 +1,38 @@ + $user->id, + 'transaction_type_id' => 2, + 'transaction_currency_id' => 1, + 'description' => 'Some journal for attachment', + 'completed' => 1, + 'date' => $start->format('Y-m-d'), + ] + ); + Transaction::create( + [ + 'account_id' => $fromAccount->id, + 'transaction_journal_id' => $journal->id, + 'amount' => -100, + + ] + ); + Transaction::create( + [ + 'account_id' => $toAccount->id, + 'transaction_journal_id' => $journal->id, + 'amount' => 100, + + ] + ); + + // and now attachments + $encrypted = Crypt::encrypt('I are secret'); + $one = Attachment::create( + [ + 'attachable_id' => $journal->id, + 'attachable_type' => 'FireflyIII\Models\TransactionJournal', + 'user_id' => $user->id, + 'md5' => md5('Hallo'), + 'filename' => 'empty-file.txt', + 'title' => 'Empty file', + 'description' => 'This file is empty', + 'notes' => 'What notes', + 'mime' => 'text/plain', + 'size' => strlen($encrypted), + 'uploaded' => 1, + ] + ); + + + // and now attachment. + $two = Attachment::create( + [ + 'attachable_id' => $journal->id, + 'attachable_type' => 'FireflyIII\Models\TransactionJournal', + 'user_id' => $user->id, + 'md5' => md5('Ook hallo'), + 'filename' => 'empty-file-2.txt', + 'title' => 'Empty file 2', + 'description' => 'This file is empty too', + 'notes' => 'What notes do', + 'mime' => 'text/plain', + 'size' => strlen($encrypted), + 'uploaded' => 1, + ] + ); + // echo crypted data to the file. + file_put_contents(storage_path('upload/at-' . $one->id . '.data'), $encrypted); + file_put_contents(storage_path('upload/at-' . $two->id . '.data'), $encrypted); + + } + /** * @param User $user */ diff --git a/app/Validation/FireflyValidator.php b/app/Validation/FireflyValidator.php index 5ee847bdf3..29672b9645 100644 --- a/app/Validation/FireflyValidator.php +++ b/app/Validation/FireflyValidator.php @@ -49,8 +49,10 @@ class FireflyValidator extends Validator */ public function validateBelongsToUser($attribute, $value, $parameters) { + $field = isset($parameters[1]) ? $parameters[1] : 'id'; - $count = DB::table($parameters[0])->where('user_id', Auth::user()->id)->where('id', $value)->count(); + + $count = DB::table($parameters[0])->where('user_id', Auth::user()->id)->where($field, $value)->count(); if ($count == 1) { return true; } diff --git a/config/firefly.php b/config/firefly.php index f37b62ce98..d22b4a31c4 100644 --- a/config/firefly.php +++ b/config/firefly.php @@ -8,11 +8,18 @@ return [ 'csv_import_enabled' => true, 'maxUploadSize' => 5242880, 'allowedMimes' => ['image/png', 'image/jpeg', 'application/pdf'], + + 'export_formats' => [ + 'csv' => 'FireflyIII\Export\Exporter\CsvExporter', + //'mt940' => 'FireflyIII\Export\Exporter\MtExporter', + ], + 'default_export_format' => 'csv', + 'piggy_bank_periods' => [ 'week' => 'Week', 'month' => 'Month', 'quarter' => 'Quarter', - 'year' => 'Year' + 'year' => 'Year', ], 'periods_to_text' => [ 'weekly' => 'A week', @@ -35,10 +42,10 @@ return [ '1M' => 'month', '3M' => 'three months', '6M' => 'half year', - 'custom' => '(custom)' + 'custom' => '(custom)', ], 'ccTypes' => [ - 'monthlyFull' => 'Full payment every month' + 'monthlyFull' => 'Full payment every month', ], 'range_to_name' => [ '1D' => 'one day', @@ -54,7 +61,7 @@ return [ '1M' => 'monthly', '3M' => 'quarterly', '6M' => 'half-year', - 'custom' => 'monthly' + 'custom' => 'monthly', ], 'subTitlesByIdentifier' => [ @@ -160,6 +167,7 @@ return [ 'tag' => 'FireflyIII\Models\Tag', 'rule' => 'FireflyIII\Models\Rule', 'ruleGroup' => 'FireflyIII\Models\RuleGroup', + 'jobKey' => 'FireflyIII\Models\ExportJob', // lists 'accountList' => 'FireflyIII\Support\Binder\AccountList', 'budgetList' => 'FireflyIII\Support\Binder\BudgetList', @@ -167,7 +175,7 @@ return [ // others 'start_date' => 'FireflyIII\Support\Binder\Date', - 'end_date' => 'FireflyIII\Support\Binder\Date' + 'end_date' => 'FireflyIII\Support\Binder\Date', ], 'rule-triggers' => [ @@ -210,6 +218,6 @@ return [ 'set_description', 'append_description', 'prepend_description', - ] + ], ]; diff --git a/public/images/loading-wide.gif b/public/images/loading-wide.gif new file mode 100644 index 0000000000000000000000000000000000000000..135987902d0ba244552677baef4dc61ca1c86e23 GIT binary patch literal 12274 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 literal 0 HcmV?d00001 diff --git a/public/js/export/index.js b/public/js/export/index.js new file mode 100644 index 0000000000..a44fe721c7 --- /dev/null +++ b/public/js/export/index.js @@ -0,0 +1,121 @@ +/* globals token, jobKey */ + +/* + * index.js + * Copyright (C) 2016 Sander Dorigo + * + * This software may be modified and distributed under the terms + * of the MIT license. See the LICENSE file for details. + */ + +var intervalId = 0; + +$(function () { + "use strict"; + // on click of export button: + // - hide form + // - post export command + // - start polling progress. + // - return false, + + $('#export').submit(startExport); + } +); + +function startExport() { + "use strict"; + console.log('Start export...'); + hideForm(); + showLoading(); + + // do export + callExport(); + + return false; +} + + +function hideForm() { + "use strict"; + $('#form-body').hide(); + $('#do-export-button').hide(); +} + +function showForm() { + "use strict"; + $('#form-body').show(); + $('#do-export-button').show(); +} + +function showLoading() { + "use strict"; + $('#export-loading').show(); +} + +function hideLoading() { + "use strict"; + $('#export-loading').hide(); +} + +function showDownload() { + "use strict"; + $('#export-download').show(); +} + +function showError(text) { + "use strict"; + $('#export-error').show(); + $('#export-error>p').text(text); +} + +function callExport() { + "use strict"; + console.log('Start callExport()...') + var data = $('#export').serialize(); + + // call status, keep calling it until response is "finished"? + intervalId = window.setInterval(checkStatus, 500); + + $.post('export/submit', data).done(function (data) { + console.log('Export hath succeeded!'); + + // stop polling: + window.clearTimeout(intervalId); + + // call it one last time: + window.setTimeout(checkStatus, 500); + + // somewhere here is a download link. + + // keep the loading thing, for debug. + hideLoading(); + + // show download + showDownload(); + + }).fail(function () { + // show error. + // show form again. + showError('The export failed. Please check the log files to find out why.'); + + // stop polling: + window.clearTimeout(intervalId); + + hideLoading(); + showForm(); + + }); +} + +function checkStatus() { + "use strict"; + console.log('get status...'); + $.getJSON('export/status/' + jobKey).done(function (data) { + putStatusText(data.status); + }); +} + +function putStatusText(status) { + "use strict"; + $('#status-message').text(status); +} \ No newline at end of file diff --git a/resources/lang/en_US/firefly.php b/resources/lang/en_US/firefly.php index 21ec2c6f79..36aa71df2d 100644 --- a/resources/lang/en_US/firefly.php +++ b/resources/lang/en_US/firefly.php @@ -38,6 +38,18 @@ return [ 'new_budget' => 'New budget', 'new_bill' => 'New bill', + // export data: + 'import_and_export' => 'Import and export', + 'export_data' => 'Export data', + 'export_data_intro' => 'For backup purposes, when migrating to another system or when migrating to another Firefly III installation.', + 'export_format' => 'Export format', + 'export_format_csv' => 'Comma separated values (CSV file)', + 'export_format_mt940' => 'MT940 compatible format', + 'export_included_accounts' => 'Export transactions from these accounts', + 'include_config_help' => 'For easy re-import into Firefly III', + 'include_old_uploads_help' => 'Firefly III does not throw away the original CSV files you have imported in the past. You can include them in your export.', + 'do_export' => 'Export', + // rules 'rules' => 'Rules', 'rules_explanation' => 'Here you can manage rules. Rules are triggered when a transaction is created or updated. Then, if the transaction has certain properties (called "triggers") Firefly will execute the "actions". Combined, you can make Firefly respond in a certain way to new transactions.', diff --git a/resources/lang/en_US/form.php b/resources/lang/en_US/form.php index d1d398ebc2..6cdb39a8bc 100644 --- a/resources/lang/en_US/form.php +++ b/resources/lang/en_US/form.php @@ -70,6 +70,12 @@ return [ 'size' => 'Size', 'trigger' => 'Trigger', 'stop_processing' => 'Stop processing', + 'start_date' => 'Start of export range', + 'end_date' => 'End of export range', + 'export_format' => 'File format', + 'include_attachments' => 'Include uploaded attachments', + 'include_config' => 'Include configuration file', + 'include_old_uploads' => 'Include imported data', 'csv_comma' => 'A comma (,)', 'csv_semicolon' => 'A semicolon (;)', diff --git a/resources/views/export/index.twig b/resources/views/export/index.twig new file mode 100644 index 0000000000..ad603cbc2d --- /dev/null +++ b/resources/views/export/index.twig @@ -0,0 +1,129 @@ +{% extends "./layout/default.twig" %} + +{% block breadcrumbs %} + {{ Breadcrumbs.renderIfExists(Route.getCurrentRoute.getName) }} +{% endblock %} + +{% block content %} + + + +
+ + + +
+
+ +
+
+

{{ 'export_data'|_ }}

+
+
+ + + +
+

+ {{ 'export_data_intro'|_ }} +

+
+
+ {{ ExpandedForm.date('start_date', first) }} + + {{ ExpandedForm.date('end_date', today) }} + + +
+ + +
+ {% if errors.has('exportFormat') %} + + {% endif %} + + {% for format in formats %} +
+ +
+ {% endfor %} +
+
+ + + + +
+ + + +
+ + {% if errors.has('accounts') %} + + {% endif %} + {% for account in accounts %} +
+ +
+ {% endfor %} +
+
+ + {{ ExpandedForm.checkbox('include_attachments','1', true) }} + + {{ ExpandedForm.checkbox('include_config','1', true, {helpText: 'include_config_help'|_}) }} + + {{ ExpandedForm.checkbox('include_old_uploads','1', false, {helpText: 'include_old_uploads_help'|_}) }} +
+
+
+
+ +
+
+
+
+ + +{% endblock %} +{% block scripts %} + + +{% endblock %} diff --git a/resources/views/partials/menu-sidebar.twig b/resources/views/partials/menu-sidebar.twig index 1683986cbc..0f82830fc7 100644 --- a/resources/views/partials/menu-sidebar.twig +++ b/resources/views/partials/menu-sidebar.twig @@ -109,6 +109,33 @@ + +
  • + + + + {% if Config.get('firefly.csv_import_enabled') %} + {{ 'import_and_export'|_ }} + {% else %} + {{ 'export_data'|_ }} + {% endif %} + + + + + +
  • +
  • @@ -127,11 +154,6 @@
  • {{ 'currencies'|_ }}
  • - {% if Config.get('firefly.csv_import_enabled') %} -
  • - {{ 'csv_import'|_ }} -
  • - {% endif %}