diff --git a/application/Espo/Core/Controllers/Record.php b/application/Espo/Core/Controllers/Record.php index be0fba0c55..c139a0cefd 100644 --- a/application/Espo/Core/Controllers/Record.php +++ b/application/Espo/Core/Controllers/Record.php @@ -258,6 +258,14 @@ class Record extends Base $params['attributeList'] = $data['attributeList']; } + if (isset($data['fieldList'])) { + $params['fieldList'] = $data['fieldList']; + } + + if (isset($data['format'])) { + $params['format'] = $data['format']; + } + return array( 'id' => $this->getRecordService()->export($params) ); diff --git a/application/Espo/Core/Export/Csv.php b/application/Espo/Core/Export/Csv.php new file mode 100644 index 0000000000..dc16acf31b --- /dev/null +++ b/application/Espo/Core/Export/Csv.php @@ -0,0 +1,65 @@ +getInjection('preferences')->get('exportDelimiter'); + if (empty($delimiter)) { + $delimiter = $this->getInjection('config')->get('exportDelimiter', ';'); + } + + $fp = fopen('php://temp', 'w'); + fputcsv($fp, $attributeList, $delimiter); + foreach ($dataList as $row) { + fputcsv($fp, $row, $delimiter); + } + rewind($fp); + $csv = stream_get_contents($fp); + fclose($fp); + + return $csv; + } +} \ No newline at end of file diff --git a/application/Espo/Core/Export/Xlsx.php b/application/Espo/Core/Export/Xlsx.php new file mode 100644 index 0000000000..ade0c58dae --- /dev/null +++ b/application/Espo/Core/Export/Xlsx.php @@ -0,0 +1,302 @@ +getInjection('config'); + } + + protected function getMetadata() + { + return $this->getInjection('metadata'); + } + + public function process($entityType, $params, $dataList) + { + if (!is_array($params['fieldList'])) { + throw new Error(); + } + + $phpExcel = new \PHPExcel(); + $sheet = $phpExcel->setActiveSheetIndex(0); + + if (isset($params['exportName'])) { + $exportName = $params['exportName']; + } else { + $exportName = $this->getInjection('language')->translate($entityType, 'scopeNamesPlural'); + } + + $sheet->setTitle($exportName); + + $fieldList = $params['fieldList']; + + $titleStyle = array( + 'alignment' => array( + 'vertical' => \PHPExcel_Style_Alignment::VERTICAL_CENTER + ), + 'font' => array( + 'bold' => true, + 'size' => 18 + ) + ); + $dateStyle = array( + 'alignment' => array( + 'vertical' => \PHPExcel_Style_Alignment::VERTICAL_CENTER + ), + 'font' => array( + 'size' => 16 + ) + ); + + $sheet->setCellValue('B1', $exportName); + + $sheet->setCellValue('C1', date('D M j G:i:s Y')); + + $sheet->getRowDimension('1')->setRowHeight(40); + $sheet->getStyle('B1')->applyFromArray($titleStyle); + $sheet->getStyle('C1')->applyFromArray($dateStyle); + + //\PHPExcel_Shared_Font::setTrueTypeFontPath('/usr/share/fonts/truetype/msttcorefonts/'); + //\PHPExcel_Shared_Font::setAutoSizeMethod(\PHPExcel_Shared_Font::AUTOSIZE_METHOD_EXACT); + + $azRange = range('A', 'Z'); + + $rowNumber = 2; + $linkColumns = []; + + foreach ($fieldList as $i => $name) { + $col = $azRange[$i]; + + $defs = $this->getInjection('metadata')->get(['entityDefs', $entityType, 'fields', $name]); + + if (!$defs) { + $defs['type'] = 'base'; + } + + $label = $this->getInjection('language')->translate($name, 'fields', $entityType); + + $sheet->setCellValue($col . $rowNumber, $label); + $sheet->getColumnDimension($col)->setAutoSize(true); + if ($defs['type'] == 'phone' + || $defs['type'] == 'email' + || $defs['type'] == 'url' + || $defs['type'] == 'link' + || $defs['type'] == 'linkParent' + || $defs['type'] == 'belongsTo' + || $defs['type'] == 'belongsToParent') { + $linkColumns[] = $col; + } else if ($name == 'name') { + if (in_array('id', $fieldList)) { + $linkColumns[] = $col; + } + } + } + $col = chr(ord($col) - 1); + $headerStyle = array( + 'font' => array( + 'bold' => true, + 'size' => 12 + ) + ); + + $sheet->getStyle("A$rowNumber:$col$rowNumber")->applyFromArray($headerStyle); + $sheet->setAutoFilter("A$rowNumber:$col$rowNumber"); + + $rowNumber++; + foreach ($dataList as $row) { + $i = 0; + foreach ($fieldList as $i => $name) { + $col = $azRange[$i]; + + $defs = $this->getInjection('metadata')->get(['entityDefs', $entityType, 'fields', $name]); + if (!$defs) { + $defs['type'] = 'base'; + } + $link = null; + if ($defs['type'] == 'link' || $defs['type'] == 'linkParent') { + $sheet->setCellValue("$col$rowNumber", $row[$name.'Name']); + } else if ($defs['type'] == 'belongsTo') { + $sheet->setCellValue("$col$rowNumber", $row[$name.'Name']); + } else if ($defs['type'] == 'belongsToParent') { + $sheet->setCellValue("$col$rowNumber", $row[$name.'Name']); + } else if ($defs['type'] == 'int') { + $sheet->setCellValue("$col$rowNumber", $row[$name] ?: 0); + } else if ($defs['type'] == 'personName') { + if (!empty($row['name'])) { + $sheet->setCellValue("$col$rowNumber", $row['name']); + } else { + $personName = ''; + if (!empty($row['firstName'])) { + $personName .= $row['firstName']; + } + if (!empty($row['lastName'])) { + if (!empty($row['firstName'])) { + $personName .= ' '; + } + $personName .= $row['lastName']; + } + $sheet->setCellValue("$col$rowNumber", $personName); + } + } else if ($defs['type'] == 'date') { + if (isset($row[$name])) { + $sheet->setCellValue("$col$rowNumber", \PHPExcel_Shared_Date::PHPToExcel(strtotime($row[$name]))); + } + } else if ($defs['type'] == 'datetime' || $defs['type'] == 'datetimeOptional') { + if (isset($row[$name])) { + $sheet->setCellValue("$col$rowNumber", \PHPExcel_Shared_Date::PHPToExcel(strtotime($row[$name]))); + } + } else if ($defs['type'] == 'image') { + + } else if ($defs['type'] == 'file') { + + } else { + if (array_key_exists($name, $row)) { + $sheet->setCellValue("$col$rowNumber", $row[$name]); + } + } + + $link = false; + + if ($name == 'name') { + if (array_key_exists('id', $row)) { + $link = $this->getConfig()->getSiteUrl() . "/#".$entityType . "/view/" . $row['id']; + } + } else if ($defs['type'] == 'url') { + if (array_key_exists($name, $row) && filter_var($row[$name], FILTER_VALIDATE_URL)) { + $link = $row[$name]; + } + } else if ($defs['type'] == 'link') { + if (array_key_exists($name.'Id', $row)) { + $foreignEntity = $this->getMetadata()->get(['entityDefs', $entityType, 'links', $name, 'entity']); + if ($foreignEntity) { + $link = $this->getConfig()->getSiteUrl() . "/#" . $foreignEntity. "/view/". $row[$name.'Id']; + } + } + } else if ($defs['type'] == 'linkParent') { + if (array_key_exists($name.'Id', $row) && array_key_exists($name.'Type', $row)) { + $link = $this->getConfig()->getSiteUrl() . "/#".$row[$name.'Type']."/view/". $row[$name.'Id']; + } + } else if ($defs['type'] == 'phone') { + if (array_key_exists($name, $row)) { + $link = "tel:".$row[$name]; + } + } else if ($defs['type'] == 'email' && array_key_exists($name, $row)) { + if (array_key_exists($name, $row)) { + $link = "mailto:".$row[$name]; + } + } + if ($link) { + $sheet->getCell("$col$rowNumber")->getHyperlink()->setUrl($link); + $sheet->getCell("$col$rowNumber")->getHyperlink()->setTooltip($link); + } + } + $rowNumber++; + } + + foreach ($fieldList as $i => $name) { + $col = $azRange[$i]; + + $defs = $this->getInjection('metadata')->get(['entityDefs', $entityType, 'fields', $name]); + if (!$defs) { + $defs['type'] = 'base'; + } + + if ($col == 'A') { + $sheet->getStyle("A2:A$rowNumber") + ->getNumberFormat() + ->setFormatCode(\PHPExcel_Style_NumberFormat::FORMAT_TEXT); + } else { + switch($defs['type']) { + case 'currency': { + $sheet->getStyle($col.'3:'.$col.$rowNumber) + ->getNumberFormat() + ->setFormatCode('£#,##0_-'); + } break; + case 'int': { + $sheet->getStyle($col.'3:'.$col.$rowNumber) + ->getNumberFormat() + ->setFormatCode('0'); + } break; + case 'date': { + $sheet->getStyle($col.'3:'.$col.$rowNumber) + ->getNumberFormat() + ->setFormatCode('dd/mm/yyyy'); + } break; + case 'datetime': { + $sheet->getStyle($col.'3:'.$col.$rowNumber) + ->getNumberFormat() + ->setFormatCode('h:mm dd/mm/yyyy'); + } break; + case 'datetimeOptional': { + $sheet->getStyle($col.'3:'.$col.$rowNumber) + ->getNumberFormat() + ->setFormatCode('h:mm dd/mm/yyyy'); + } break; + default: { + $sheet->getStyle($col.'3:'.$col.$rowNumber) + ->getNumberFormat() + ->setFormatCode('@'); + } break; + } + } + } + + $linkStyle = [ + 'font' => [ + 'color' => ['rgb' => '0000FF'], + 'underline' => 'single' + ] + ]; + foreach ($linkColumns as $linkColumn) { + $sheet->getStyle($linkColumn.'3:'.$linkColumn.$rowNumber)->applyFromArray($linkStyle); + } + + $tempOutput = tempnam('/tmp/', 'ESPO'); + $objWriter = \PHPExcel_IOFactory::createWriter($phpExcel, 'Excel2007'); + $objWriter->save($tempOutput); + $fp = fopen($tempOutput, 'r'); + $xlsx = stream_get_contents($fp); + unlink($tempOutput); + + return $xlsx; + + } +} \ No newline at end of file diff --git a/application/Espo/Resources/i18n/en_US/Export.json b/application/Espo/Resources/i18n/en_US/Export.json index 45123240c9..0bbc938150 100644 --- a/application/Espo/Resources/i18n/en_US/Export.json +++ b/application/Espo/Resources/i18n/en_US/Export.json @@ -1,6 +1,13 @@ { "fields": { - "useCustomFieldList": "Custom Field List", - "fieldList": "Field List" + "exportAllFields": "Export all fields", + "fieldList": "Field List", + "format": "Format" + }, + "options": { + "format": { + "csv": "CSV", + "xlsx": "XLSX (Excel)" + } } } diff --git a/application/Espo/Resources/metadata/app/export.json b/application/Espo/Resources/metadata/app/export.json new file mode 100644 index 0000000000..40a353dea2 --- /dev/null +++ b/application/Espo/Resources/metadata/app/export.json @@ -0,0 +1,20 @@ +{ + "formatList": [ + "csv", + "xlsx" + ], + "formatDefs": { + "csv": { + "mimeType": "text/csv", + "fileExtension": "csv" + }, + "xlsx": { + "mimeType": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", + "fileExtension": "xlsx" + } + }, + "exportFormatClassNameMap": { + "csv": "\\Espo\\Core\\Export\\Csv", + "xlsx": "\\Espo\\Core\\Export\\Xlsx" + } +} \ No newline at end of file diff --git a/application/Espo/Services/Record.php b/application/Espo/Services/Record.php index fb6b733dac..6f10501128 100644 --- a/application/Espo/Services/Record.php +++ b/application/Espo/Services/Record.php @@ -53,7 +53,8 @@ class Record extends \Espo\Core\Services\Base 'fileManager', 'selectManagerFactory', 'preferences', - 'fileStorageManager' + 'fileStorageManager', + 'injectableFactory' ); protected $getEntityBeforeUpdate = false; @@ -1271,6 +1272,16 @@ class Record extends \Espo\Core\Services\Base public function export(array $params) { + if (array_key_exists('format', $params)) { + $format = $params['format']; + } else { + $format = 'csv'; + } + + if (!in_array($format, $this->getMetadata()->get(['app', 'export', 'formatList']))) { + throw new Error('Not supported export format.'); + } + if (array_key_exists('collection', $params)) { $collection = $params['collection']; } else { @@ -1364,31 +1375,46 @@ class Record extends \Espo\Core\Services\Base $arr[] = $row; } - $delimiter = $this->getPreferences()->get('exportDelimiter'); - if (empty($delimiter)) { - $delimiter = $this->getConfig()->get('exportDelimiter', ';'); + if (is_null($attributeList)) { + $attributeList = []; } - $fp = fopen('php://temp', 'w'); - fputcsv($fp, array_keys($arr[0]), $delimiter); - foreach ($arr as $row) { - fputcsv($fp, $row, $delimiter); + if (!array_key_exists('fieldList', $params)) { + $fieldList = array_keys($fieldList); + array_unshift($fieldList, 'id'); + } else { + $fieldList = $params['fieldList']; } - rewind($fp); - $csv = stream_get_contents($fp); - fclose($fp); - $fileName = "Export_{$this->entityType}.csv"; + $mimeType = $this->getMetadata()->get(['app', 'export', 'formatDefs', $format, 'mimeType']); + $fileExtension = $this->getMetadata()->get(['app', 'export', 'formatDefs', $format, 'fileExtension']); + $fileName = "Export_{$this->entityType}." . $fileExtension; + + $className = $this->getMetadata()->get(['app', 'export', 'exportFormatClassNameMap', $format]); + if (empty($className)) { + throw new Error(); + } + $exportObj = $this->getInjection('injectableFactory')->createByClassName($className); + $exportParams = array( + 'attributeList' => $attributeList, + 'fileName ' => $fileName + ); + + $exportParams['fieldList'] = $fieldList; + if (array_key_exists('exportName', $params)) { + $exportParams['exportName'] = $params['exportName']; + } + $contents = $exportObj->process($this->entityType, $exportParams, $arr); $attachment = $this->getEntityManager()->getEntity('Attachment'); $attachment->set('name', $fileName); $attachment->set('role', 'Export File'); - $attachment->set('type', 'text/csv'); + $attachment->set('type', $mimeType); $this->getEntityManager()->saveEntity($attachment); if (!empty($attachment->id)) { - $this->getInjection('fileStorageManager')->putContents($attachment, $csv); + $this->getInjection('fileStorageManager')->putContents($attachment, $contents); // TODO cron job to remove file return $attachment->id; } diff --git a/client/res/templates/export/record/record.tpl b/client/res/templates/export/record/record.tpl index 7c13a0cbb2..9f16a24d5d 100644 --- a/client/res/templates/export/record/record.tpl +++ b/client/res/templates/export/record/record.tpl @@ -1,7 +1,14 @@ -
- -
{{{useCustomFieldList}}}
+
+ +
{{{format}}}
-
+ +
+ +
{{{exportAllFields}}}
+
+ +
+
{{{fieldList}}}
diff --git a/client/src/views/export/modals/export.js b/client/src/views/export/modals/export.js index 41b4f9115f..1bdcb3920b 100644 --- a/client/src/views/export/modals/export.js +++ b/client/src/views/export/modals/export.js @@ -59,7 +59,11 @@ Espo.define('views/export/modals/export', ['views/modal', 'model'], function (De if (this.options.fieldList) { this.model.set('fieldList', this.options.fieldList); - this.model.set('useCustomFieldList', true); + this.model.set('exportAllFields', false); + this.model.set('format', 'csv'); + } else { + this.model.set('exportAllFields', true); + this.model.set('format', 'csv'); } this.createView('record', 'views/export/record/record', { @@ -75,10 +79,11 @@ Espo.define('views/export/modals/export', ['views/modal', 'model'], function (De if (this.getView('record').validate()) return; var returnData = { - useCustomFieldList: data.useCustomFieldList + exportAllFields: data.exportAllFields, + format: data.format }; - if (data.useCustomFieldList) { + if (!data.exportAllFields) { var attributeList = []; data.fieldList.forEach(function (item) { if (item === 'id') { diff --git a/client/src/views/export/record/record.js b/client/src/views/export/record/record.js index 8dd8bd6ff3..44e220a354 100644 --- a/client/src/views/export/record/record.js +++ b/client/src/views/export/record/record.js @@ -64,7 +64,7 @@ Espo.define('views/export/record/record', 'views/record/base', function (Dep) { translatedOptions[item] = this.getLanguage().translate(item, 'fields', this.scope); }, this); - this.createField('useCustomFieldList', 'views/fields/bool', { + this.createField('exportAllFields', 'views/fields/bool', { }); var setFieldList = this.model.get('fieldList') || []; @@ -79,7 +79,6 @@ Espo.define('views/export/record/record', 'views/record/base', function (Dep) { var foreignScope = this.getMetadata().get(['entityDefs', this.scope, 'links', arr[0], 'entity']); if (!foreignScope) return; translatedOptions[item] = this.getLanguage().translate(arr[0], 'links', this.scope) + '.' + this.getLanguage().translate(arr[1], 'fields', foreignScope); - }, this); @@ -89,17 +88,24 @@ Espo.define('views/export/record/record', 'views/record/base', function (Dep) { options: fieldList }); - this.controlVisibility(); - this.listenTo(this.model, 'change:useCustomFieldList', function () { - this.controlVisibility(); + this.createField('format', 'views/fields/enum', { + options: this.getMetadata().get('app.export.formatList') + }); + + this.controlAllFields(); + this.listenTo(this.model, 'change:exportAllFields', function () { + this.controlAllFields(); }, this); }, - controlVisibility: function () { - if (this.model.get('useCustomFieldList')) { + controlAllFields: function () { + if (!this.model.get('exportAllFields')) { this.showField('fieldList'); + //this.setFieldOptionList('format', this.getMetadata().get('app.export.formatList')); } else { this.hideField('fieldList'); + //this.model.set('format', 'csv'); + //this.setFieldOptionList('format', ['csv']); } } diff --git a/client/src/views/record/list.js b/client/src/views/record/list.js index d839ca0d48..62e6c5073a 100644 --- a/client/src/views/record/list.js +++ b/client/src/views/record/list.js @@ -359,10 +359,11 @@ Espo.define('views/record/list', 'view', function (Dep) { this.createView('dialogExport', 'views/export/modals/export', o, function (view) { view.render(); this.listenToOnce(view, 'proceed', function (dialogData) { - if (dialogData.useCustomFieldList) { + if (!dialogData.exportAllFields) { data.attributeList = dialogData.attributeList; data.fieldList = dialogData.fieldList; } + data.format = dialogData.format; this.ajaxPostRequest(url, data).then(function (data) { if ('id' in data) { window.location = this.getBasePath() + '?entryPoint=download&id=' + data.id; diff --git a/composer.json b/composer.json index 904bd42863..dbac262c3c 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,8 @@ "zendframework/zend-servicemanager": "2.6.0", "tecnickcom/tcpdf": "^6.2", "php-mime-mail-parser/php-mime-mail-parser": "^2.5", - "zbateson/mail-mime-parser": "^0.4.1" + "zbateson/mail-mime-parser": "^0.4.1", + "phpoffice/phpexcel": "^1.8" }, "autoload": { "psr-0": { diff --git a/composer.lock b/composer.lock index e20d166c0e..5efd821363 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,8 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "6e1e3327b6081f402280ed7ad0c324f3", - "content-hash": "c98074c42af6b9e3396ae058bc602bea", + "hash": "064c61d7e728851cad5d703c85ade74d", + "content-hash": "02c2bb32ee1f881379ee82f5d375419d", "packages": [ { "name": "composer/semver", @@ -766,6 +766,63 @@ ], "time": "2016-10-07 16:46:22" }, + { + "name": "phpoffice/phpexcel", + "version": "1.8.1", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/PHPExcel.git", + "reference": "372c7cbb695a6f6f1e62649381aeaa37e7e70b32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/PHPExcel/zipball/372c7cbb695a6f6f1e62649381aeaa37e7e70b32", + "reference": "372c7cbb695a6f6f1e62649381aeaa37e7e70b32", + "shasum": "" + }, + "require": { + "ext-xml": "*", + "ext-xmlwriter": "*", + "php": ">=5.2.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "PHPExcel": "Classes/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL" + ], + "authors": [ + { + "name": "Maarten Balliauw", + "homepage": "http://blog.maartenballiauw.be" + }, + { + "name": "Mark Baker" + }, + { + "name": "Franck Lefevre", + "homepage": "http://blog.rootslabs.net" + }, + { + "name": "Erik Tilt" + } + ], + "description": "PHPExcel - OpenXML - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", + "homepage": "http://phpexcel.codeplex.com", + "keywords": [ + "OpenXML", + "excel", + "php", + "spreadsheet", + "xls", + "xlsx" + ], + "time": "2015-05-01 07:00:55" + }, { "name": "psr/log", "version": "1.0.0",