Files
espocrm/application/Espo/Services/Import.php
2016-10-19 12:14:21 +03:00

654 lines
22 KiB
PHP

<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2015 Yuri Kuznetsov, Taras Machyshyn, Oleksiy Avramenko
* Website: http://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Services;
use \Espo\Core\Exceptions\Forbidden;
use \Espo\Core\Exceptions\NotFound;
use \Espo\Core\Exceptions\Error;
use \Espo\Core\Exceptions\BadRequest;
use Espo\ORM\Entity;
class Import extends \Espo\Services\Record
{
protected function init()
{
parent::init();
$this->addDependency('serviceFactory');
$this->addDependency('fileManager');
$this->addDependency('selectManagerFactory');
}
protected $dateFormatsMap = array(
'YYYY-MM-DD' => 'Y-m-d',
'DD-MM-YYYY' => 'd-m-Y',
'MM-DD-YYYY' => 'm-d-Y',
'MM/DD/YYYY' => 'm/d/Y',
'DD/MM/YYYY' => 'd/m/Y',
'DD.MM.YYYY' => 'd.m.Y',
'MM.DD.YYYY' => 'm.d.Y',
'YYYY.MM.DD' => 'Y.m.d',
);
protected $timeFormatsMap = array(
'HH:mm' => 'H:i',
'hh:mm a' => 'h:i a',
'hh:mma' => 'h:ia',
'hh:mm A' => 'h:iA',
'hh:mmA' => 'h:iA',
);
protected $services = array();
protected function getSelectManagerFactory()
{
return $this->injections['selectManagerFactory'];
}
protected function getFileManager()
{
return $this->injections['fileManager'];
}
protected function getAcl()
{
return $this->injections['acl'];
}
protected function getMetadata()
{
return $this->injections['metadata'];
}
protected function getServiceFactory()
{
return $this->injections['serviceFactory'];
}
public function loadAdditionalFields(Entity $entity)
{
parent::loadAdditionalFields($entity);
$importedCount = $this->getRepository()->countRelated($entity, 'imported');
$duplicateCount = $this->getRepository()->countRelated($entity, 'duplicates');
$updatedCount = $this->getRepository()->countRelated($entity, 'updated');
$entity->set(array(
'importedCount' => $importedCount,
'duplicateCount' => $duplicateCount,
'updatedCount' => $updatedCount,
));
}
public function findLinkedEntities($id, $link, $params)
{
$entity = $this->getRepository()->get($id);
$foreignEntityName = $entity->get('entityType');
if (!$this->getAcl()->check($entity, 'read')) {
throw new Forbidden();
}
if (!$this->getAcl()->check($foreignEntityName, 'read')) {
throw new Forbidden();
}
$selectParams = $this->getSelectManager($foreignEntityName)->getSelectParams($params, true);
if (array_key_exists($link, $this->linkSelectParams)) {
$selectParams = array_merge($selectParams, $this->linkSelectParams[$link]);
}
$collection = $this->getRepository()->findRelated($entity, $link, $selectParams);
$recordService = $this->getRecordService($foreignEntityName);
foreach ($collection as $e) {
$recordService->loadAdditionalFieldsForList($e);
$recordService->prepareEntityForOutput($e);
}
$total = $this->getRepository()->countRelated($entity, $link, $selectParams);
return array(
'total' => $total,
'collection' => $collection
);
}
protected function readCsvString(&$string, $CSV_SEPARATOR = ';', $CSV_ENCLOSURE = '"', $CSV_LINEBREAK = "\n")
{
$o = array();
$cnt = strlen($string);
$esc = false;
$escesc = false;
$num = 0;
$i = 0;
while ($i < $cnt) {
$s = $string[$i];
if ($s == $CSV_LINEBREAK) {
if ($esc) {
$o[$num].= $s;
}
else {
$i++;
break;
}
}
elseif ($s == $CSV_SEPARATOR) {
if ($esc) {
$o[$num].= $s;
}
else {
$num++;
$esc = false;
$escesc = false;
}
}
elseif ($s == $CSV_ENCLOSURE) {
if ($escesc) {
$o[$num].= $CSV_ENCLOSURE;
$escesc = false;
}
if ($esc) {
$esc = false;
$escesc = true;
}
else {
$esc = true;
$escesc = false;
}
}
else {
if (!array_key_exists($num, $o)) {
$o[$num] = '';
}
if ($escesc) {
$o[$num] .= $CSV_ENCLOSURE;
$escesc = false;
}
$o[$num] .= $s;
}
$i++;
}
$string = substr($string, $i);
return $o;
}
public function revert($id)
{
$import = $this->getEntityManager()->getEntity('Import', $id);
if (empty($import)) {
throw new NotFound();
}
$pdo = $this->getEntityManager()->getPDO();
$sql = "SELECT * FROM import_entity WHERE import_id = ".$pdo->quote($import->id) . " AND is_imported = 1";
$sth = $pdo->prepare($sql);
$sth->execute();
while ($row = $sth->fetch(\PDO::FETCH_ASSOC)) {
if (empty($row['entity_type']) || empty($row['entity_id'])) {
continue;
}
$entityType = $row['entity_type'];
$entityId = $row['entity_id'];
$entity = $this->getEntityManager()->getEntity($entityType, $entityId);
if ($entity) {
$this->getEntityManager()->removeEntity($entity);
}
$this->getEntityManager()->getRepository($entityType)->deleteFromDb($entityId);
}
$this->getEntityManager()->removeEntity($import);
return true;
}
public function removeDuplicates($id)
{
$import = $this->getEntityManager()->getEntity('Import', $id);
if (empty($import)) {
throw new NotFound();
}
$pdo = $this->getEntityManager()->getPDO();
$sql = "SELECT * FROM import_entity WHERE import_id = ".$pdo->quote($import->id) . " AND is_duplicate = 1";
$sth = $pdo->prepare($sql);
$sth->execute();
while ($row = $sth->fetch(\PDO::FETCH_ASSOC)) {
if (empty($row['entity_type']) || empty($row['entity_id'])) {
continue;
}
$entityType = $row['entity_type'];
$entityId = $row['entity_id'];
$entity = $this->getEntityManager()->getEntity($entityType, $entityId);
if ($entity) {
$this->getEntityManager()->removeEntity($entity);
}
$this->getEntityManager()->getRepository($scope)->deleteFromDb($entityId);
}
return true;
}
public function import($scope, array $importFieldList, $attachmentId, array $params = array())
{
$delimiter = ',';
if (!empty($params['fieldDelimiter'])) {
$delimiter = $params['fieldDelimiter'];
}
$enclosure = '"';
if (!empty($params['textQualifier'])) {
$enclosure = $params['textQualifier'];
}
$contents = $this->getFileManager()->getContents('data/upload/' . $attachmentId);
if (empty($contents)) {
throw new Error('Import error');
}
$import = $this->getEntityManager()->getEntity('Import');
$import->set(array(
'entityType' => $scope,
'fileId' => $attachmentId
));
$this->getEntityManager()->saveEntity($import);
$pdo = $this->getEntityManager()->getPDO();
$result = array(
'importedIds' => array(),
'updatedIds' => array(),
'duplicateIds' => array(),
);
$i = -1;
$contents = str_replace("\r\n", "\n", $contents);
while ($arr = $this->readCsvString($contents, $delimiter, $enclosure)) {
$i++;
if ($i == 0 && !empty($params['headerRow'])) {
continue;
}
if (count($arr) == 1 && empty($arr[0])) {
continue;
}
$r = $this->importRow($scope, $importFieldList, $arr, $params);
if (empty($r)) {
continue;
}
if (!empty($r['isImported'])) {
$result['importedIds'][] = $r['id'];
}
if (!empty($r['isUpdated'])) {
$result['updatedIds'][] = $r['id'];
}
if (!empty($r['isDuplicate'])) {
$result['duplicateIds'][] = $r['id'];
}
$sql = "
INSERT INTO import_entity
(entity_type, entity_id, import_id, is_imported, is_updated, is_duplicate)
VALUES
(:entityType, :entityId, :importId, :isImported, :isUpdated, :isDuplicate)
";
$sth = $pdo->prepare($sql);
$sth->bindValue(':entityType', $scope);
$sth->bindValue(':entityId', $r['id']);
$sth->bindValue(':importId', $import->id);
$sth->bindValue(':isImported', !empty($r['isImported']), \PDO::PARAM_BOOL);
$sth->bindValue(':isUpdated', !empty($r['isUpdated']), \PDO::PARAM_BOOL);
$sth->bindValue(':isDuplicate', !empty($r['isDuplicate']), \PDO::PARAM_BOOL);
$sth->execute();
}
return array(
'id' => $import->id,
'countCreated' => count($result['importedIds']),
'countUpdated' => count($result['updatedIds']),
);
}
public function importRow($scope, array $importFieldList, array $row, array $params = array())
{
$id = null;
$action = 'create';
if (!empty($params['action'])) {
$action = $params['action'];
}
if (empty($importFieldList)) {
return;
}
if (in_array($action, ['createAndUpdate', 'update'])) {
if (!empty($params['updateBy']) && is_array($params['updateBy'])) {
$updateByFieldList = [];
$whereClause = array();
foreach ($params['updateBy'] as $i) {
if (array_key_exists($i, $importFieldList)) {
$updateByFieldList[] = $importFieldList[$i];
$whereClause[$importFieldList[$i]] = $row[$i];
}
}
}
}
$recordService = $this->getRecordService($scope);
if (in_array($action, ['createAndUpdate', 'update'])) {
if (!count($updateByFieldList)) {
return;
}
$entity = $this->getEntityManager()->getRepository($scope)->where($whereClause)->findOne();
if (!$entity) {
if ($action == 'createAndUpdate') {
$entity = $this->getEntityManager()->getEntity($scope);
if (array_key_exists('id', $whereClause)) {
$entity->set('id', $whereClause['id']);
}
} else {
return;
}
}
} else {
$entity = $this->getEntityManager()->getEntity($scope);
}
$isNew = $entity->isNew();
if (!empty($params['defaultValues'])) {
$v = get_object_vars($params['defaultValues']);
$entity->set($v);
}
$fieldsDefs = $entity->getFields();
$relDefs = $entity->getRelations();
$phoneFieldList = [];
if (!empty($fieldsDefs['phoneNumber']) && !empty($fieldsDefs['phoneNumber']['type']) && $fieldsDefs['phoneNumber']['type'] == 'phone') {
$typeList = $this->getMetadata()->get('entityDefs.' . $scope . '.fields.phoneNumber.typeList', []);
foreach ($typeList as $type) {
$attr = str_replace(' ', '_', ucfirst($type));
$phoneFieldList[] = 'phoneNumber' . $attr;
}
}
foreach ($importFieldList as $i => $field) {
if (!empty($field)) {
if (!array_key_exists($i, $row)) {
continue;
}
$value = $row[$i];
if ($field == 'id') {
if ($params['action'] == 'create') {
$entity->id = $value;
}
continue;
}
if (array_key_exists($field, $fieldsDefs)) {
if ($value !== '') {
$type = $this->getMetadata()->get("entityDefs.{$scope}.fields.{$field}.type");
if ($type == 'personName') {
$lastNameField = 'last' . ucfirst($field);
$firstNameField = 'first' . ucfirst($field);
$firstName = '';
$lastName = $value;
switch ($params['personNameFormat']) {
case 'f l':
$pos = strpos($value, ' ');
if ($pos) {
$firstName = trim(substr($value, 0, $pos));
$lastName = trim(substr($value, $pos + 1));
}
break;
case 'l f':
$pos = strpos($value, ' ');
if ($pos) {
$lastName = trim(substr($value, 0, $pos));
$firstName = trim(substr($value, $pos + 1));
}
break;
case 'l, f':
$pos = strpos($value, ',');
if ($pos) {
$lastName = trim(substr($value, 0, $pos));
$firstName = trim(substr($value, $pos + 1));
}
break;
}
if (!$entity->get($firstNameField)) {
$entity->set($firstNameField, $firstName);
}
if (!$entity->get($lastNameField)) {
$entity->set($lastNameField, $lastName);
}
continue;
}
$entity->set($field, $this->parseValue($entity, $field, $value, $params));
}
} else {
if (in_array($field, $phoneFieldList) && !empty($value)) {
$phoneNumberData = $entity->get('phoneNumberData');
$isPrimary = false;
if (empty($phoneNumberData)) {
$phoneNumberData = [];
if (!in_array('phoneNumber', $importFieldList)) {
$isPrimary = true;
}
}
$type = str_replace('phoneNumber', '', $field);
$type = str_replace('_', ' ', $type);
$o = new \StdClass();
$o->phoneNumber = $value;
$o->type = $type;
$o->primary = $isPrimary;
$phoneNumberData[] = $o;
$entity->set('phoneNumberData', $phoneNumberData);
}
}
}
}
foreach ($importFieldList as $i => $field) {
if (!array_key_exists($field, $fieldsDefs)) continue;;
$defs = $fieldsDefs[$field];
$type = $fieldsDefs[$field]['type'];
if (in_array($type, [Entity::FOREIGN, Entity::VARCHAR]) && !empty($defs['foreign']) && $defs['foreign'] === 'name') {
if ($entity->has($field)) {
$relation = $defs['relation'];
if ($field == $relation . 'Name' && !$entity->has($relation . 'Id') && array_key_exists($relation, $relDefs)) {
if ($relDefs[$relation]['type'] == Entity::BELONGS_TO) {
$name = $entity->get($field);
$scope = $relDefs[$relation]['entity'];
$found = $this->getEntityManager()->getRepository($scope)->where(array('name' => $name))->findOne();
if ($found) {
$entity->set($relation . 'Id', $found->id);
} else {
if (!in_array($scope, 'User', 'Team')) {
// TODO create related record with name $name and relate
}
}
}
}
}
}
}
$result = array();
$a = $entity->toArray();
try {
if ($isNew) {
$isDuplicate = $recordService->checkEntityForDuplicate($entity);
}
$saveResult = $this->getEntityManager()->saveEntity($entity, array(
'noStream' => true,
'noNotifications' => true,
'import' => true
));
if ($saveResult) {
$result['id'] = $entity->id;
if ($isNew) {
$result['isImported'] = true;
if ($isDuplicate) {
$result['isDuplicate'] = true;
}
} else {
$result['isUpdated'] = true;
}
}
} catch (\Exception $e) {
$GLOBALS['log']->error('Import: [' . $e->getCode() . '] ' .$e->getMessage());
}
return $result;
}
protected function parseValue(Entity $entity, $field, $value, $params = array())
{
$decimalMark = '.';
if (!empty($params['decimalMark'])) {
$decimalMark = $params['decimalMark'];
}
$defaultCurrency = 'USD';
if (!empty($params['defaultCurrency'])) {
$defaultCurrency = $params['defaultCurrency'];
}
$dateFormat = 'Y-m-d';
if (!empty($params['dateFormat'])) {
if (!empty($this->dateFormatsMap[$params['dateFormat']])) {
$dateFormat = $this->dateFormatsMap[$params['dateFormat']];
}
}
$timeFormat = 'H:i';
if (!empty($params['timeFormat'])) {
if (!empty($this->timeFormatsMap[$params['timeFormat']])) {
$timeFormat = $this->timeFormatsMap[$params['timeFormat']];
}
}
$fieldDefs = $entity->getFields();
if (!empty($fieldDefs[$field])) {
$type = $fieldDefs[$field]['type'];
switch ($type) {
case Entity::DATE:
$dt = \DateTime::createFromFormat($dateFormat, $value);
if ($dt) {
return $dt->format('Y-m-d');
}
break;
case Entity::DATETIME:
$dt = \DateTime::createFromFormat($dateFormat . ' ' . $timeFormat, $value);
if ($dt) {
return $dt->format('Y-m-d H:i');
}
break;
case Entity::FLOAT:
$currencyField = $field . 'Currency';
if ($entity->hasField($currencyField)) {
if (!$entity->has($currencyField)) {
$entity->set($currencyField, $defaultCurrency);
}
}
$a = explode($decimalMark, $value);
$a[0] = preg_replace('/[^A-Za-z0-9\-]/', '', $a[0]);
if (count($a) > 1) {
return $a[0] . '.' . $a[1];
} else {
return $a[0];
}
break;
}
}
return $value;
}
protected function getRecordService($scope)
{
if (empty($this->services[$scope])) {
if ($this->getServiceFactory()->checkExists($scope)) {
$service = $this->getServiceFactory()->create($scope);
} else {
$service = $this->getServiceFactory()->create('Record');
$service->setEntityType($scope);
}
$this->services[$scope] = $service;
}
return $this->services[$scope];
}
public function unmarkAsDuplicate($id, $entityType, $entityId)
{
$pdo = $this->getEntityManager()->getPDO();
$sql = "
UPDATE import_entity
SET is_duplicate = 0
WHERE
import_id = ".$pdo->quote($id)." AND
entity_type = ".$pdo->quote($entityType)." AND
entity_id = ".$pdo->quote($entityId)."
";
if ($pdo->query($sql)) {
return true;
}
}
}