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 @@ -