From b35190b7e36cfd6b79525a7550d3984dc825d093 Mon Sep 17 00:00:00 2001 From: Alex Lushpai Date: Mon, 20 Jul 2015 17:59:27 +0300 Subject: [PATCH 01/17] rebranding, api url|key emptiness error fix, icml fixes --- composer.json | 35 - composer.lock | 67 -- intarocrm/config.xml | 13 - intarocrm/export.tpl | 40 - intarocrm/logo.gif | Bin 1124 -> 0 bytes intarocrm/logo.png | Bin 1963 -> 0 bytes retailcrm/classes/Icml.php | 136 +++ retailcrm/classes/Retailcrm.php | 1055 +++++++++++++++++ retailcrm/classes/Service.php | 43 + retailcrm/config.xml | 13 + retailcrm/export.tpl | 31 + {intarocrm => retailcrm}/index.php | 6 +- retailcrm/logo.gif | Bin 0 -> 306 bytes retailcrm/logo.png | Bin 0 -> 957 bytes .../intarocrm.php => retailcrm/retailcrm.php | 373 +++--- {intarocrm => retailcrm}/sync.php | 4 +- {intarocrm => retailcrm}/translations/ru.php | 0 17 files changed, 1479 insertions(+), 337 deletions(-) delete mode 100644 composer.json delete mode 100644 composer.lock delete mode 100644 intarocrm/config.xml delete mode 100644 intarocrm/export.tpl delete mode 100644 intarocrm/logo.gif delete mode 100644 intarocrm/logo.png create mode 100644 retailcrm/classes/Icml.php create mode 100644 retailcrm/classes/Retailcrm.php create mode 100644 retailcrm/classes/Service.php create mode 100644 retailcrm/config.xml create mode 100644 retailcrm/export.tpl rename {intarocrm => retailcrm}/index.php (58%) create mode 100644 retailcrm/logo.gif create mode 100644 retailcrm/logo.png rename intarocrm/intarocrm.php => retailcrm/retailcrm.php (83%) rename {intarocrm => retailcrm}/sync.php (71%) rename {intarocrm => retailcrm}/translations/ru.php (100%) diff --git a/composer.json b/composer.json deleted file mode 100644 index 5b855e4..0000000 --- a/composer.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "name": "intarocrm/prestashop-module", - "description": "Prestashop integration for IntaroCRM", - "type": "library", - "keywords": ["api", "Intaro CRM", "rest"], - "homepage": "http://www.intarocrm.ru/", - "config": { - "vendor-dir": "intarocrm/classes" - }, - "authors": [ - { - "name": "Alex Lushpai", - "email": "lushpai@intaro.ru", - "role": "Developer" - } - ], - "support": { - "email": "support@intarocrm.ru" - }, - "require": { - "php": ">=5.3", - "intarocrm/rest-api-client": "1.2.*" - }, - "autoload": { - "psr-0": { - "": "src/" - } - }, - "repositories": [ - { - "type": "git", - "url": "https://github.com/intarocrm/rest-api-client" - } - ] -} diff --git a/composer.lock b/composer.lock deleted file mode 100644 index 239a739..0000000 --- a/composer.lock +++ /dev/null @@ -1,67 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" - ], - "hash": "e4583dae732a6e5ebafff8cca46b1984", - "packages": [ - { - "name": "intarocrm/rest-api-client", - "version": "v1.2.5", - "source": { - "type": "git", - "url": "https://github.com/intarocrm/rest-api-client", - "reference": "b54350ff2f09d8202cf2931895bba8dced4dcf21" - }, - "require": { - "ext-curl": "*", - "php": ">=5.2.0" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-0": { - "IntaroCrm\\": "lib/" - } - }, - "authors": [ - { - "name": "Kruglov Kirill", - "email": "kruglov@intaro.ru", - "role": "Developer" - } - ], - "description": "PHP Client for IntaroCRM REST API", - "homepage": "http://www.intarocrm.ru/", - "keywords": [ - "Intaro CRM", - "api", - "rest" - ], - "support": { - "email": "support@intarocrm.ru" - }, - "time": "2014-04-13 09:58:37" - } - ], - "packages-dev": [ - - ], - "aliases": [ - - ], - "minimum-stability": "stable", - "stability-flags": [ - - ], - "platform": { - "php": ">=5.3" - }, - "platform-dev": [ - - ] -} diff --git a/intarocrm/config.xml b/intarocrm/config.xml deleted file mode 100644 index dcb2d94..0000000 --- a/intarocrm/config.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - intarocrm - - - - - - Вы уверены, что хотите удалить модуль? - 1 - 1 - - \ No newline at end of file diff --git a/intarocrm/export.tpl b/intarocrm/export.tpl deleted file mode 100644 index ed065b6..0000000 --- a/intarocrm/export.tpl +++ /dev/null @@ -1,40 +0,0 @@ - - - - {$shop_name} - {$yamarket_company_name} - {$shop_url} - PrestaShop - - {foreach from=$currencies item=cur name=currencies} - {if $cur.iso_code != 'GBP'} - - {/if} - {/foreach} - - - {foreach from=$categories item=cat name=categories} - {if $cat.id_category!=2} - {$cat.name} - {/if} - {/foreach} - - {if $yamarket_delivery_price}{$yamarket_delivery_price}{/if} - - {foreach from=$products item=offer name=products} - - {$offer.url} - {$offer.price} - {$offer.purchase_price} - {$currency} - {$offer.id_category_default} - {$offer.picture} - {$offer.name} - {if $offer.article} - {$offer.article} - {/if} - - {/foreach} - - - \ No newline at end of file diff --git a/intarocrm/logo.gif b/intarocrm/logo.gif deleted file mode 100644 index c9abff4af5b30e99274d8d056c86e98e156c5b7b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1124 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 diff --git a/retailcrm/classes/Icml.php b/retailcrm/classes/Icml.php new file mode 100644 index 0000000..94009cd --- /dev/null +++ b/retailcrm/classes/Icml.php @@ -0,0 +1,136 @@ +shop = $shop; + $this->file = $file; + + $this->properties = array( + 'name', + 'productName', + 'initialPrice', + 'purchasePrice', + 'vendor', + 'picture', + 'url', + 'xmlId', + 'productActivity' + ); + + $this->params = array( + 'article' => 'Артикул', + 'color' => 'Цвет', + 'weight' => 'Вес', + 'size' => 'Размер', + ); + } + + public function generate($categories, $offers) + { + $string = ' + + + '.$this->shop.' + + + + + '; + + $xml = new SimpleXMLElement( + $string, + LIBXML_NOENT |LIBXML_NOCDATA | LIBXML_COMPACT | LIBXML_PARSEHUGE + ); + + $this->dd = new DOMDocument(); + $this->dd->preserveWhiteSpace = false; + $this->dd->formatOutput = true; + $this->dd->loadXML($xml->asXML()); + + $this->eCategories = $this->dd + ->getElementsByTagName('categories')->item(0); + $this->eOffers = $this->dd + ->getElementsByTagName('offers')->item(0); + + $this->addCategories($categories); + $this->addOffers($offers); + + $this->dd->saveXML(); + $this->dd->save($this->file); + } + + private function addCategories($categories) + { + foreach($categories as $category) { + $e = $this->eCategories->appendChild( + $this->dd->createElement( + 'category', $category['name'] + ) + ); + + $e->setAttribute('id', $category['id']); + + if ($category['parentId'] > 0) { + $e->setAttribute('parentId', $category['parentId']); + } + } + } + + private function addOffers($offers) + { + foreach ($offers as $offer) { + + $e = $this->eOffers->appendChild( + $this->dd->createElement('offer') + ); + + $e->setAttribute('id', $offer['id']); + $e->setAttribute('productId', $offer['productId']); + + if (!empty($offer['quantity'])) { + $e->setAttribute('quantity', (int) $offer['quantity']); + } else { + $e->setAttribute('quantity', 0); + } + + foreach ($offer['categoryId'] as $categoryId) { + $e->appendChild( + $this->dd->createElement('categoryId', $categoryId) + ); + } + + $offerKeys = array_keys($offer); + + foreach ($offerKeys as $key) { + if (in_array($key, $this->properties)) { + $e->appendChild( + $this->dd->createElement($key) + )->appendChild( + $this->dd->createTextNode($offer[$key]) + ); + } + + if (in_array($key, array_keys($this->params))) { + $param = $this->dd->createElement('param'); + $param->setAttribute('code', $key); + $param->setAttribute('name', $this->params[$key]); + $param->appendChild( + $this->dd->createTextNode($offer[$key]) + ); + $e->appendChild($param); + } + } + } + } +} + diff --git a/retailcrm/classes/Retailcrm.php b/retailcrm/classes/Retailcrm.php new file mode 100644 index 0000000..4276019 --- /dev/null +++ b/retailcrm/classes/Retailcrm.php @@ -0,0 +1,1055 @@ +url = $url; + $this->defaultParameters = $defaultParameters; + $this->retry = 0; + } + + /** + * Make HTTP request + * + * @param string $path + * @param string $method (default: 'GET') + * @param array $parameters (default: array()) + * @param int $timeout + * @param bool $verify + * @param bool $debug + * @return ApiResponse + */ + public function makeRequest( + $path, + $method, + array $parameters = array(), + $timeout = 30, + $verify = false, + $debug = false + ) { + $allowedMethods = array(self::METHOD_GET, self::METHOD_POST); + if (!in_array($method, $allowedMethods)) { + throw new InvalidArgumentException(sprintf( + 'Method "%s" is not valid. Allowed methods are %s', + $method, + implode(', ', $allowedMethods) + )); + } + + $parameters = array_merge($this->defaultParameters, $parameters); + + $url = $this->url . $path; + + if (self::METHOD_GET === $method && sizeof($parameters)) { + $url .= '?' . http_build_query($parameters); + } + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); + curl_setopt($ch, CURLOPT_FAILONERROR, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $verify); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $verify); + + if (!$debug) { + curl_setopt($ch, CURLOPT_TIMEOUT, (int) $timeout); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, (int) $timeout); + } else { + curl_setopt($ch, CURLOPT_TIMEOUT_MS, (int) $timeout + ($this->retry * 2000)); + } + + if (self::METHOD_POST === $method) { + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $parameters); + } + + $responseBody = curl_exec($ch); + $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $errno = curl_errno($ch); + $error = curl_error($ch); + + curl_close($ch); + + if ($errno && in_array($errno, array(6, 7, 28, 34, 35)) && $this->retry < 3) { + $errno = null; + $error = null; + $this->retry += 1; + $this->makeRequest( + $path, + $method, + $parameters, + $timeout, + $verify, + $debug + ); + } + + if ($errno) { + throw new CurlException($error, $errno); + } + + return new ApiResponse($statusCode, $responseBody); + } + + public function getRetry() + { + return $this->retry; + } +} + +/** + * Response from retailCRM API + */ +class ApiResponse implements ArrayAccess +{ + // HTTP response status code + protected $statusCode; + + // response assoc array + protected $response; + + public function __construct($statusCode, $responseBody = null) + { + $this->statusCode = (int) $statusCode; + + if (!empty($responseBody)) { + $response = json_decode($responseBody, true); + + if (!$response && JSON_ERROR_NONE !== ($error = json_last_error())) { + throw new InvalidJsonException( + "Invalid JSON in the API response body. Error code #$error", + $error + ); + } + + $this->response = $response; + } + } + + /** + * Return HTTP response status code + * + * @return int + */ + public function getStatusCode() + { + return $this->statusCode; + } + + /** + * HTTP request was successful + * + * @return bool + */ + public function isSuccessful() + { + return $this->statusCode < 400; + } + + /** + * Allow to access for the property throw class method + * + * @param string $name + * @return mixed + */ + public function __call($name, $arguments) + { + // convert getSomeProperty to someProperty + $propertyName = strtolower(substr($name, 3, 1)) . substr($name, 4); + + if (!isset($this->response[$propertyName])) { + throw new InvalidArgumentException("Method \"$name\" not found"); + } + + return $this->response[$propertyName]; + } + + /** + * Allow to access for the property throw object property + * + * @param string $name + * @return mixed + */ + public function __get($name) + { + if (!isset($this->response[$name])) { + throw new InvalidArgumentException("Property \"$name\" not found"); + } + + return $this->response[$name]; + } + + /** + * @param mixed $offset + * @param mixed $value + */ + public function offsetSet($offset, $value) + { + throw new BadMethodCallException('This activity not allowed'); + } + + /** + * @param mixed $offset + */ + public function offsetUnset($offset) + { + throw new BadMethodCallException('This call not allowed'); + } + + /** + * @param mixed $offset + * @return bool + */ + public function offsetExists($offset) + { + return isset($this->response[$offset]); + } + + /** + * @param mixed $offset + * @return mixed + */ + public function offsetGet($offset) + { + if (!isset($this->response[$offset])) { + throw new InvalidArgumentException("Property \"$offset\" not found"); + } + + return $this->response[$offset]; + } +} + + +/** + * retailCRM API client class + */ +class ApiClient +{ + const VERSION = 'v3'; + + protected $client; + + /** + * Site code + */ + protected $siteCode; + + /** + * Client creating + * + * @param string $url + * @param string $apiKey + * @param string $siteCode + * @return mixed + */ + public function __construct($url, $apiKey, $site = null) + { + if ('/' != substr($url, strlen($url) - 1, 1)) { + $url .= '/'; + } + + $url = $url . 'api/' . self::VERSION; + + $this->client = new Client($url, array('apiKey' => $apiKey)); + $this->siteCode = $site; + } + + /** + * Create a order + * + * @param array $order + * @param string $site (default: null) + * @return ApiResponse + */ + public function ordersCreate(array $order, $site = null) + { + if (!sizeof($order)) { + throw new InvalidArgumentException('Parameter `order` must contains a data'); + } + + return $this->client->makeRequest("/orders/create", Client::METHOD_POST, $this->fillSite($site, array( + 'order' => json_encode($order) + ))); + } + + /** + * Edit a order + * + * @param array $order + * @param string $site (default: null) + * @return ApiResponse + */ + public function ordersEdit(array $order, $by = 'externalId', $site = null) + { + if (!sizeof($order)) { + throw new InvalidArgumentException('Parameter `order` must contains a data'); + } + + $this->checkIdParameter($by); + + if (!isset($order[$by])) { + throw new InvalidArgumentException(sprintf('Order array must contain the "%s" parameter.', $by)); + } + + return $this->client->makeRequest( + "/orders/" . $order[$by] . "/edit", + Client::METHOD_POST, + $this->fillSite($site, array( + 'order' => json_encode($order), + 'by' => $by, + )) + ); + } + + /** + * Upload array of the orders + * + * @param array $orders + * @param string $site (default: null) + * @return ApiResponse + */ + public function ordersUpload(array $orders, $site = null) + { + if (!sizeof($orders)) { + throw new InvalidArgumentException('Parameter `orders` must contains array of the orders'); + } + + return $this->client->makeRequest("/orders/upload", Client::METHOD_POST, $this->fillSite($site, array( + 'orders' => json_encode($orders), + ))); + } + + /** + * Get order by id or externalId + * + * @param string $id + * @param string $by (default: 'externalId') + * @param string $site (default: null) + * @return ApiResponse + */ + public function ordersGet($id, $by = 'externalId', $site = null) + { + $this->checkIdParameter($by); + + return $this->client->makeRequest("/orders/$id", Client::METHOD_GET, $this->fillSite($site, array( + 'by' => $by + ))); + } + + /** + * Returns a orders history + * + * @param DateTime $startDate (default: null) + * @param DateTime $endDate (default: null) + * @param int $limit (default: 100) + * @param int $offset (default: 0) + * @param bool $skipMyChanges (default: true) + * @return ApiResponse + */ + public function ordersHistory( + DateTime $startDate = null, + DateTime $endDate = null, + $limit = 100, + $offset = 0, + $skipMyChanges = true + ) { + $parameters = array(); + + if ($startDate) { + $parameters['startDate'] = $startDate->format('Y-m-d H:i:s'); + } + if ($endDate) { + $parameters['endDate'] = $endDate->format('Y-m-d H:i:s'); + } + if ($limit) { + $parameters['limit'] = (int) $limit; + } + if ($offset) { + $parameters['offset'] = (int) $offset; + } + if ($skipMyChanges) { + $parameters['skipMyChanges'] = (bool) $skipMyChanges; + } + + return $this->client->makeRequest('/orders/history', Client::METHOD_GET, $parameters); + } + + /** + * Returns filtered orders list + * + * @param array $filter (default: array()) + * @param int $page (default: null) + * @param int $limit (default: null) + * @return ApiResponse + */ + public function ordersList(array $filter = array(), $page = null, $limit = null) + { + $parameters = array(); + + if (sizeof($filter)) { + $parameters['filter'] = $filter; + } + if (null !== $page) { + $parameters['page'] = (int) $page; + } + if (null !== $limit) { + $parameters['limit'] = (int) $limit; + } + + return $this->client->makeRequest('/orders', Client::METHOD_GET, $parameters); + } + + /** + * Returns statuses of the orders + * + * @param array $ids (default: array()) + * @param array $externalIds (default: array()) + * @return ApiResponse + */ + public function ordersStatuses(array $ids = array(), array $externalIds = array()) + { + $parameters = array(); + + if (sizeof($ids)) { + $parameters['ids'] = $ids; + } + if (sizeof($externalIds)) { + $parameters['externalIds'] = $externalIds; + } + + return $this->client->makeRequest('/orders/statuses', Client::METHOD_GET, $parameters); + } + + /** + * Save order IDs' (id and externalId) association in the CRM + * + * @param array $ids + * @return ApiResponse + */ + public function ordersFixExternalIds(array $ids) + { + if (!sizeof($ids)) { + throw new InvalidArgumentException('Method parameter must contains at least one IDs pair'); + } + + return $this->client->makeRequest("/orders/fix-external-ids", Client::METHOD_POST, array( + 'orders' => json_encode($ids), + )); + } + + /** + * Get orders assembly history + * + * @param array $filter (default: array()) + * @param int $page (default: null) + * @param int $limit (default: null) + * @return ApiResponse + */ + public function ordersPacksHistory(array $filter = array(), $page = null, $limit = null) + { + $parameters = array(); + + if (sizeof($filter)) { + $parameters['filter'] = $filter; + } + if (null !== $page) { + $parameters['page'] = (int) $page; + } + if (null !== $limit) { + $parameters['limit'] = (int) $limit; + } + + return $this->client->makeRequest('/orders/packs/history', Client::METHOD_GET, $parameters); + } + + /** + * Create a customer + * + * @param array $customer + * @param string $site (default: null) + * @return ApiResponse + */ + public function customersCreate(array $customer, $site = null) + { + if (!sizeof($customer)) { + throw new InvalidArgumentException('Parameter `customer` must contains a data'); + } + + return $this->client->makeRequest("/customers/create", Client::METHOD_POST, $this->fillSite($site, array( + 'customer' => json_encode($customer) + ))); + } + + /** + * Edit a customer + * + * @param array $customer + * @param string $by (default: 'externalId') + * @param string $site (default: null) + * @return ApiResponse + */ + public function customersEdit(array $customer, $by = 'externalId', $site = null) + { + if (!sizeof($customer)) { + throw new InvalidArgumentException('Parameter `customer` must contains a data'); + } + + $this->checkIdParameter($by); + + if (!isset($customer[$by])) { + throw new InvalidArgumentException(sprintf('Customer array must contain the "%s" parameter.', $by)); + } + + return $this->client->makeRequest( + "/customers/" . $customer[$by] . "/edit", + Client::METHOD_POST, + $this->fillSite( + $site, + array( + 'customer' => json_encode($customer), + 'by' => $by + ) + ) + ); + } + + /** + * Upload array of the customers + * + * @param array $customers + * @param string $site (default: null) + * @return ApiResponse + */ + public function customersUpload(array $customers, $site = null) + { + if (!sizeof($customers)) { + throw new InvalidArgumentException('Parameter `customers` must contains array of the customers'); + } + + return $this->client->makeRequest("/customers/upload", Client::METHOD_POST, $this->fillSite($site, array( + 'customers' => json_encode($customers), + ))); + } + + /** + * Get customer by id or externalId + * + * @param string $id + * @param string $by (default: 'externalId') + * @param string $site (default: null) + * @return ApiResponse + */ + public function customersGet($id, $by = 'externalId', $site = null) + { + $this->checkIdParameter($by); + + return $this->client->makeRequest("/customers/$id", Client::METHOD_GET, $this->fillSite($site, array( + 'by' => $by + ))); + } + + /** + * Returns filtered customers list + * + * @param array $filter (default: array()) + * @param int $page (default: null) + * @param int $limit (default: null) + * @return ApiResponse + */ + public function customersList(array $filter = array(), $page = null, $limit = null) + { + $parameters = array(); + + if (sizeof($filter)) { + $parameters['filter'] = $filter; + } + if (null !== $page) { + $parameters['page'] = (int) $page; + } + if (null !== $limit) { + $parameters['limit'] = (int) $limit; + } + + return $this->client->makeRequest('/customers', Client::METHOD_GET, $parameters); + } + + /** + * Save customer IDs' (id and externalId) association in the CRM + * + * @param array $ids + * @return ApiResponse + */ + public function customersFixExternalIds(array $ids) + { + if (!sizeof($ids)) { + throw new InvalidArgumentException('Method parameter must contains at least one IDs pair'); + } + + return $this->client->makeRequest("/customers/fix-external-ids", Client::METHOD_POST, array( + 'customers' => json_encode($ids), + )); + } + + /** + * Get purchace prices & stock balance + * + * @param array $filter (default: array()) + * @param int $page (default: null) + * @param int $limit (default: null) + * @param string $site (default: null) + * @return ApiResponse + */ + public function storeInventories(array $filter = array(), $page = null, $limit = null, $site = null) + { + $parameters = array(); + + if (sizeof($filter)) { + $parameters['filter'] = $filter; + } + if (null !== $page) { + $parameters['page'] = (int) $page; + } + if (null !== $limit) { + $parameters['limit'] = (int) $limit; + } + + return $this->client->makeRequest('/store/inventories', Client::METHOD_GET, $this->fillSite($site, $parameters)); + } + + /** + * Upload store inventories + * + * @param array $offers + * @param string $site (default: null) + * @return ApiResponse + */ + public function storeInventoriesUpload(array $offers, $site = null) + { + if (!sizeof($offers)) { + throw new InvalidArgumentException('Parameter `offers` must contains array of the customers'); + } + + return $this->client->makeRequest( + "/store/inventories/upload", + Client::METHOD_POST, + $this->fillSite($site, array('offers' => json_encode($offers))) + ); + } + + /** + * Returns deliveryServices list + * + * @return ApiResponse + */ + public function deliveryServicesList() + { + return $this->client->makeRequest('/reference/delivery-services', Client::METHOD_GET); + } + + /** + * Returns deliveryTypes list + * + * @return ApiResponse + */ + public function deliveryTypesList() + { + return $this->client->makeRequest('/reference/delivery-types', Client::METHOD_GET); + } + + /** + * Returns orderMethods list + * + * @return ApiResponse + */ + public function orderMethodsList() + { + return $this->client->makeRequest('/reference/order-methods', Client::METHOD_GET); + } + + /** + * Returns orderTypes list + * + * @return ApiResponse + */ + public function orderTypesList() + { + return $this->client->makeRequest('/reference/order-types', Client::METHOD_GET); + } + + /** + * Returns paymentStatuses list + * + * @return ApiResponse + */ + public function paymentStatusesList() + { + return $this->client->makeRequest('/reference/payment-statuses', Client::METHOD_GET); + } + + /** + * Returns paymentTypes list + * + * @return ApiResponse + */ + public function paymentTypesList() + { + return $this->client->makeRequest('/reference/payment-types', Client::METHOD_GET); + } + + /** + * Returns productStatuses list + * + * @return ApiResponse + */ + public function productStatusesList() + { + return $this->client->makeRequest('/reference/product-statuses', Client::METHOD_GET); + } + + /** + * Returns statusGroups list + * + * @return ApiResponse + */ + public function statusGroupsList() + { + return $this->client->makeRequest('/reference/status-groups', Client::METHOD_GET); + } + + /** + * Returns statuses list + * + * @return ApiResponse + */ + public function statusesList() + { + return $this->client->makeRequest('/reference/statuses', Client::METHOD_GET); + } + + /** + * Returns sites list + * + * @return ApiResponse + */ + public function sitesList() + { + return $this->client->makeRequest('/reference/sites', Client::METHOD_GET); + } + + /** + * Returns stores list + * + * @return ApiResponse + */ + public function storesList() + { + return $this->client->makeRequest('/reference/stores', Client::METHOD_GET); + } + + /** + * Edit deliveryService + * + * @param array $data delivery service data + * @return ApiResponse + */ + public function deliveryServicesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/delivery-services/' . $data['code'] . '/edit', + Client::METHOD_POST, + array( + 'deliveryService' => json_encode($data) + ) + ); + } + + /** + * Edit deliveryType + * + * @param array $data delivery type data + * @return ApiResponse + */ + public function deliveryTypesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/delivery-types/' . $data['code'] . '/edit', + Client::METHOD_POST, + array( + 'deliveryType' => json_encode($data) + ) + ); + } + + /** + * Edit orderMethod + * + * @param array $data order method data + * @return ApiResponse + */ + public function orderMethodsEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/order-methods/' . $data['code'] . '/edit', + Client::METHOD_POST, + array( + 'orderMethod' => json_encode($data) + ) + ); + } + + /** + * Edit orderType + * + * @param array $data order type data + * @return ApiResponse + */ + public function orderTypesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/order-types/' . $data['code'] . '/edit', + Client::METHOD_POST, + array( + 'orderType' => json_encode($data) + ) + ); + } + + /** + * Edit paymentStatus + * + * @param array $data payment status data + * @return ApiResponse + */ + public function paymentStatusesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/payment-statuses/' . $data['code'] . '/edit', + Client::METHOD_POST, + array( + 'paymentStatus' => json_encode($data) + ) + ); + } + + /** + * Edit paymentType + * + * @param array $data payment type data + * @return ApiResponse + */ + public function paymentTypesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/payment-types/' . $data['code'] . '/edit', + Client::METHOD_POST, + array( + 'paymentType' => json_encode($data) + ) + ); + } + + /** + * Edit productStatus + * + * @param array $data product status data + * @return ApiResponse + */ + public function productStatusesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/product-statuses/' . $data['code'] . '/edit', + Client::METHOD_POST, + array( + 'productStatus' => json_encode($data) + ) + ); + } + + /** + * Edit order status + * + * @param array $data status data + * @return ApiResponse + */ + public function statusesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/statuses/' . $data['code'] . '/edit', + Client::METHOD_POST, + array( + 'status' => json_encode($data) + ) + ); + } + + /** + * Edit site + * + * @param array $data site data + * @return ApiResponse + */ + public function sitesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/sites/' . $data['code'] . '/edit', + Client::METHOD_POST, + array( + 'site' => json_encode($data) + ) + ); + } + + /** + * Edit store + * + * @param array $data site data + * @return ApiResponse + */ + public function storesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + if (!isset($data['name'])) { + throw new InvalidArgumentException('Data must contain "name" parameter.'); + } + + return $this->client->makeRequest( + '/reference/stores/' . $data['code'] . '/edit', + Client::METHOD_POST, + array( + 'store' => json_encode($data) + ) + ); + } + + /** + * Update CRM basic statistic + * + * @return ApiResponse + */ + public function statisticUpdate() + { + return $this->client->makeRequest('/statistic/update', Client::METHOD_GET); + } + + /** + * Return current site + * + * @return string + */ + public function getSite() + { + return $this->siteCode; + } + + /** + * Set site + * + * @param string $site + * @return void + */ + public function setSite($site) + { + $this->siteCode = $site; + } + + /** + * Check ID parameter + * + * @param string $by + * @return bool + */ + protected function checkIdParameter($by) + { + $allowedForBy = array('externalId', 'id'); + if (!in_array($by, $allowedForBy)) { + throw new InvalidArgumentException(sprintf( + 'Value "%s" for parameter "by" is not valid. Allowed values are %s.', + $by, + implode(', ', $allowedForBy) + )); + } + + return true; + } + + /** + * Fill params by site value + * + * @param string $site + * @param array $params + * @return array + */ + protected function fillSite($site, array $params) + { + if ($site) { + $params['site'] = $site; + } elseif ($this->siteCode) { + $params['site'] = $this->siteCode; + } + + return $params; + } +} + +class InvalidJsonException extends DomainException +{ +} + +class CurlException extends RuntimeException +{ +} diff --git a/retailcrm/classes/Service.php b/retailcrm/classes/Service.php new file mode 100644 index 0000000..98d13a0 --- /dev/null +++ b/retailcrm/classes/Service.php @@ -0,0 +1,43 @@ + + + retailcrm + + + + + + + 1 + 1 + + \ No newline at end of file diff --git a/retailcrm/export.tpl b/retailcrm/export.tpl new file mode 100644 index 0000000..ead0361 --- /dev/null +++ b/retailcrm/export.tpl @@ -0,0 +1,31 @@ + + + + {$shop_name|escape} + {$company|escape} + {$shop_url|escape} + + {foreach from=$categories item=cat name=categories} + {if $cat.id_category > 2} + {$cat.name} + {/if} + {/foreach} + + + {foreach from=$products item=offer name=products} + + {if $offer.available_for_order}Y{else}N{/if} + {$offer.url|escape} + {$offer.price} + {$offer.purchase_price} + {$offer.id_category_default} + {$offer.picture|escape} + {$offer.name|escape} + {if $offer.article} + {$offer.article|escape} + {/if} + + {/foreach} + + + diff --git a/intarocrm/index.php b/retailcrm/index.php similarity index 58% rename from intarocrm/index.php rename to retailcrm/index.php index 5f95ef6..5f8745d 100644 --- a/intarocrm/index.php +++ b/retailcrm/index.php @@ -1,9 +1,9 @@ exportCatalog(); \ No newline at end of file +$export = new RetailCRM(); +echo $export->exportCatalog(); diff --git a/retailcrm/logo.gif b/retailcrm/logo.gif new file mode 100644 index 0000000000000000000000000000000000000000..ef6236111871375750f1465b1fc52dce815a8f62 GIT binary patch literal 306 zcmZ?wbhEHb6krfwSZc)ZQBv}gl+-7A`OgLhpG{0Y+u41MjQkoE^({XBM}7UTRjYoj zTlZ_@#$VgF|K73V*Up{4cklkaXV33_`+o1=|L5Sr--i$XK6>=e(WAeQ9s2`B$B+Lx zdGgQMvwyE$`+MWY-Le;+;i`}py{SFirPdGqh{=l|cn z{r~af|IeTQfBpLZ`}hApfBygd`~Tm+|3m{F1|R_W#epr{VP=7cj*#)DMt3pCKp(e< z9m>{$i(RItF-2-@c#v^?ss)GlgBwp{u+@-wy!5K6(1W1uzy^S~?gNAeNUEzn^JZxp@?Wr@)?+){7T%eVv^-q9L7%V00{5+1u0SB?R z2Egd;O*SiM6BuQ(z5QBxB2Xu%;mtj5f00E++BJ9jpB11(jSlLpCj;;uuCa)UA%u! z)z#iBSCraXr(WZ6I~rvT4QeP<{u|^TKCHNRPdRnU;rjH+jz+Pw!%mz~J34;OopK1| zmoIX4)pPYK5!th|=7R@FB*^Dut*xxC4ylw_SU@77wzXA0d7=aYE*kyrK($`Jl%@%Q zzkZ$6v{Wc`{Es~bo}%->=OZF(bd-ds7#w70&+d-+OmD9@9A=G;1SlSlH8DY)S3Z91 z@{xsb6g+p1bX`WHbp1LJDfRW$Z{Fk|^m)DN<;yN50_=D1L@WkCZfzM|U8#!~)7P%q zD=Wz5V45&Z`R!Zg&K*88Q>H}frAvHr5}AzJ)>ifEmHhC*92&}Wb&-^$Cbo!`?Mname = 'intarocrm'; + $this->name = 'retailcrm'; $this->tab = 'market_place'; - $this->version = '0.1'; - $this->author = 'Intaro Ltd.'; + $this->version = '0.2'; + $this->author = 'Retail Driver LCC'; - $this->displayName = $this->l('IntaroCRM'); - $this->description = $this->l('Integration module for IntaroCRM'); + $this->displayName = $this->l('RetailCRM'); + $this->description = $this->l('Integration module for RetailCRM'); $this->confirmUninstall = $this->l('Are you sure you want to uninstall?'); - $this->intaroCRM = new \IntaroCrm\RestApi( - Configuration::get('INTAROCRM_ADDRESS'), - Configuration::get('INTAROCRM_API_TOKEN') - ); + $this->apiUrl = Configuration::get('RETAILCRM_ADDRESS'); + $this->apiKey = Configuration::get('RETAILCRM_API_TOKEN'); + + if (!empty($this->apiUrl) && !empty($this->apiKey)) { + $this->api = new ApiClient( + $this->apiUrl, + $this->apiKey + ); + } $this->default_lang = (int)Configuration::get('PS_LANG_DEFAULT'); $this->default_currency = (int)Configuration::get('PS_CURRENCY_DEFAULT'); @@ -54,12 +61,12 @@ class IntaroCRM extends Module function uninstall() { return parent::uninstall() && - Configuration::deleteByName('INTAROCRM_ADDRESS') && - Configuration::deleteByName('INTAROCRM_API_TOKEN') && - Configuration::deleteByName('INTAROCRM_API_STATUS') && - Configuration::deleteByName('INTAROCRM_API_DELIVERY') && - Configuration::deleteByName('INTAROCRM_LAST_SYNC') && - Configuration::deleteByName('INTAROCRM_API_ADDR') + Configuration::deleteByName('RETAILCRM_ADDRESS') && + Configuration::deleteByName('RETAILCRM_API_TOKEN') && + Configuration::deleteByName('RETAILCRM_API_STATUS') && + Configuration::deleteByName('RETAILCRM_API_DELIVERY') && + Configuration::deleteByName('RETAILCRM_LAST_SYNC') && + Configuration::deleteByName('RETAILCRM_API_ADDR') ; } @@ -67,8 +74,8 @@ class IntaroCRM extends Module { $output = null; - $address = Configuration::get('INTAROCRM_ADDRESS'); - $token = Configuration::get('INTAROCRM_API_TOKEN'); + $address = Configuration::get('RETAILCRM_ADDRESS'); + $token = Configuration::get('RETAILCRM_API_TOKEN'); if (!$address || $address == '') { $output .= $this->displayError( $this->l('Invalid crm address') ); @@ -83,24 +90,24 @@ class IntaroCRM extends Module if (Tools::isSubmit('submit'.$this->name)) { - $address = strval(Tools::getValue('INTAROCRM_ADDRESS')); - $token = strval(Tools::getValue('INTAROCRM_API_TOKEN')); - $delivery = json_encode(Tools::getValue('INTAROCRM_API_DELIVERY')); - $status = json_encode(Tools::getValue('INTAROCRM_API_STATUS')); - $payment = json_encode(Tools::getValue('INTAROCRM_API_PAYMENT')); - $order_address = json_encode(Tools::getValue('INTAROCRM_API_ADDR')); + $address = strval(Tools::getValue('RETAILCRM_ADDRESS')); + $token = strval(Tools::getValue('RETAILCRM_API_TOKEN')); + $delivery = json_encode(Tools::getValue('RETAILCRM_API_DELIVERY')); + $status = json_encode(Tools::getValue('RETAILCRM_API_STATUS')); + $payment = json_encode(Tools::getValue('RETAILCRM_API_PAYMENT')); + $order_address = json_encode(Tools::getValue('RETAILCRM_API_ADDR')); if (!$address || empty($address) || !Validate::isGenericName($address)) { $output .= $this->displayError( $this->l('Invalid crm address') ); } elseif (!$token || empty($token) || !Validate::isGenericName($token)) { $output .= $this->displayError( $this->l('Invalid crm api token') ); } else { - Configuration::updateValue('INTAROCRM_ADDRESS', $address); - Configuration::updateValue('INTAROCRM_API_TOKEN', $token); - Configuration::updateValue('INTAROCRM_API_DELIVERY', $delivery); - Configuration::updateValue('INTAROCRM_API_STATUS', $status); - Configuration::updateValue('INTAROCRM_API_PAYMENT', $payment); - Configuration::updateValue('INTAROCRM_API_ADDR', $order_address); + Configuration::updateValue('RETAILCRM_ADDRESS', $address); + Configuration::updateValue('RETAILCRM_API_TOKEN', $token); + Configuration::updateValue('RETAILCRM_API_DELIVERY', $delivery); + Configuration::updateValue('RETAILCRM_API_STATUS', $status); + Configuration::updateValue('RETAILCRM_API_PAYMENT', $payment); + Configuration::updateValue('RETAILCRM_API_ADDR', $order_address); $output .= $this->displayConfirmation($this->l('Settings updated')); } } @@ -115,7 +122,7 @@ class IntaroCRM extends Module $this->displayConfirmation($this->l('Settings updated')); $default_lang = $this->default_lang; - $intaroCrm = $this->intaroCRM; + $intaroCrm = $this->api; /* * Network connection form @@ -128,14 +135,14 @@ class IntaroCRM extends Module array( 'type' => 'text', 'label' => $this->l('CRM address'), - 'name' => 'INTAROCRM_ADDRESS', + 'name' => 'RETAILCRM_ADDRESS', 'size' => 20, 'required' => true ), array( 'type' => 'text', 'label' => $this->l('CRM token'), - 'name' => 'INTAROCRM_API_TOKEN', + 'name' => 'RETAILCRM_API_TOKEN', 'size' => 20, 'required' => true ) @@ -146,35 +153,38 @@ class IntaroCRM extends Module ) ); - /* - * Delivery - */ - $fields_form[1]['form'] = array( - 'legend' => array( - 'title' => $this->l('Delivery'), - ), - 'input' => $this->getDeliveryTypes($default_lang, $intaroCrm), - ); - /* - * Order status - */ - $fields_form[2]['form'] = array( - 'legend' => array( - 'title' => $this->l('Order statuses'), - ), - 'input' => $this->getStatusTypes($default_lang, $intaroCrm), - ); + if (!empty($this->apiUrl) && !empty($this->apiKey)) { + /* + * Delivery + */ + $fields_form[1]['form'] = array( + 'legend' => array( + 'title' => $this->l('Delivery'), + ), + 'input' => $this->getDeliveryTypes(), + ); - /* - * Payment - */ - $fields_form[3]['form'] = array( - 'legend' => array( - 'title' => $this->l('Payment types'), - ), - 'input' => $this->getPaymentTypes($intaroCrm), - ); + /* + * Order status + */ + $fields_form[2]['form'] = array( + 'legend' => array( + 'title' => $this->l('Order statuses'), + ), + 'input' => $this->getStatusTypes(), + ); + + /* + * Payment + */ + $fields_form[3]['form'] = array( + 'legend' => array( + 'title' => $this->l('Payment types'), + ), + 'input' => $this->getPaymentTypes(), + ); + } /* * Address fields @@ -218,45 +228,45 @@ class IntaroCRM extends Module ) ); - $helper->fields_value['INTAROCRM_ADDRESS'] = Configuration::get('INTAROCRM_ADDRESS'); - $helper->fields_value['INTAROCRM_API_TOKEN'] = Configuration::get('INTAROCRM_API_TOKEN'); + $helper->fields_value['RETAILCRM_ADDRESS'] = Configuration::get('RETAILCRM_ADDRESS'); + $helper->fields_value['RETAILCRM_API_TOKEN'] = Configuration::get('RETAILCRM_API_TOKEN'); - $deliverySettings = Configuration::get('INTAROCRM_API_DELIVERY'); + $deliverySettings = Configuration::get('RETAILCRM_API_DELIVERY'); if (isset($deliverySettings) && $deliverySettings != '') { $deliveryTypes = json_decode($deliverySettings); foreach ($deliveryTypes as $idx => $delivery) { - $name = 'INTAROCRM_API_DELIVERY[' . $idx . ']'; + $name = 'RETAILCRM_API_DELIVERY[' . $idx . ']'; $helper->fields_value[$name] = $delivery; } } - $statusSettings = Configuration::get('INTAROCRM_API_STATUS'); + $statusSettings = Configuration::get('RETAILCRM_API_STATUS'); if (isset($statusSettings) && $statusSettings != '') { $statusTypes = json_decode($statusSettings); foreach ($statusTypes as $idx => $status) { - $name = 'INTAROCRM_API_STATUS[' . $idx . ']'; + $name = 'RETAILCRM_API_STATUS[' . $idx . ']'; $helper->fields_value[$name] = $status; } } - $paymentSettings = Configuration::get('INTAROCRM_API_PAYMENT'); + $paymentSettings = Configuration::get('RETAILCRM_API_PAYMENT'); if (isset($paymentSettings) && $paymentSettings != '') { $paymentTypes = json_decode($paymentSettings); foreach ($paymentTypes as $idx => $payment) { - $name = 'INTAROCRM_API_PAYMENT[' . $idx . ']'; + $name = 'RETAILCRM_API_PAYMENT[' . $idx . ']'; $helper->fields_value[$name] = $payment; } } - $addressSettings = Configuration::get('INTAROCRM_API_ADDR'); + $addressSettings = Configuration::get('RETAILCRM_API_ADDR'); if (isset($addressSettings) && $addressSettings != '') { $addressTypes = json_decode($addressSettings); foreach ($addressTypes as $idx => $address) { - $name = 'INTAROCRM_API_ADDR[' . $idx . ']'; + $name = 'RETAILCRM_API_ADDR[' . $idx . ']'; $helper->fields_value[$name] = $address; } } @@ -271,13 +281,14 @@ class IntaroCRM extends Module public function hookActionPaymentConfirmation($params) { - $this->intaroCRM->orderEdit( + $this->api->ordersEdit( array( 'externalId' => $params['id_order'], 'paymentStatus' => 'paid', 'createdAt' => $params['cart']->date_upd ) ); + return $this->hookActionOrderStatusPostUpdate($params); } @@ -287,13 +298,13 @@ class IntaroCRM extends Module $sql = 'SELECT * FROM '._DB_PREFIX_.'address WHERE id_address='.(int)$address_id; $address = Db::getInstance()->ExecuteS($sql); $address = $address[0]; - $delivery = json_decode(Configuration::get('INTAROCRM_API_DELIVERY')); - $payment = json_decode(Configuration::get('INTAROCRM_API_PAYMENT')); + $delivery = json_decode(Configuration::get('RETAILCRM_API_DELIVERY')); + $payment = json_decode(Configuration::get('RETAILCRM_API_PAYMENT')); $inCart = $params['cart']->getProducts(); if (isset($params['orderStatus'])) { try { - $crmCustomerId = $this->intaroCRM->customerCreate( + $this->api->customersCreate( array( 'externalId' => $params['cart']->id_customer, 'lastName' => $params['customer']->lastname, @@ -313,11 +324,11 @@ class IntaroCRM extends Module ) ); } - catch (\IntaroCrm\Exception\CurlException $e) { - error_log("customerCreate: connection error", 3, "intarocrm.log"); + catch (CurlException $e) { + error_log("customerCreate: connection error", 3, _PS_ROOT_DIR . "log/retailcrm.log"); } - catch (\IntaroCrm\Exception\ApiException $e) { - error_log('customerCreate: ' . $e->getMessage(), 3, "intarocrm.log"); + catch (InvalidJsonException $e) { + error_log('customerCreate: ' . $e->getMessage(), 3, _PS_ROOT_DIR . "log/retailcrm.log"); } try { @@ -338,7 +349,7 @@ class IntaroCRM extends Module } else { $pTypeKey = $params['order']->payment; } - $this->intaroCRM->orderCreate( + $this->api->ordersCreate( array( 'externalId' => $params['order']->id, 'orderType' => 'eshop-individual', @@ -364,23 +375,23 @@ class IntaroCRM extends Module ) ); } - catch (\IntaroCrm\Exception\CurlException $e) { - error_log('orderCreate: connection error', 3, "intarocrm.log"); + catch (CurlException $e) { + error_log('orderCreate: connection error', 3, _PS_ROOT_DIR . "log/retailcrm.log"); } - catch (\IntaroCrm\Exception\ApiException $e) { - error_log('orderCreate: ' . $e->getMessage(), 3, "intarocrm.log"); + catch (InvalidJsonException $e) { + error_log('orderCreate: ' . $e->getMessage(), 3, _PS_ROOT_DIR . "log/retailcrm.log"); } } if (isset($params['newOrderStatus']) && !empty($params['newOrderStatus'])) { $statuses = OrderState::getOrderStates($this->default_lang); - $aStatuses = json_decode(Configuration::get('INTAROCRM_API_STATUS')); + $aStatuses = json_decode(Configuration::get('RETAILCRM_API_STATUS')); foreach ($statuses as $status) { if ($status['name'] == $params['newOrderStatus']->name) { $currStatus = $status['id_order_state']; try { - $this->intaroCRM->orderEdit( + $this->api->ordersEdit( array( 'externalId' => $params['id_order'], 'status' => $aStatuses->$currStatus, @@ -388,38 +399,40 @@ class IntaroCRM extends Module ) ); } - catch (\IntaroCrm\Exception\CurlException $e) { - error_log('orderStatusUpdate: connection error', 3, "intarocrm.log"); + catch (CurlException $e) { + error_log('orderStatusUpdate: connection error', 3, _PS_ROOT_DIR . "log/retailcrm.log"); } - catch (\IntaroCrm\Exception\ApiException $e) { - error_log('orderStatusUpdate: ' . $e->getMessage(), 3, "intarocrm.log"); + catch (InvalidJsonException $e) { + error_log('orderStatusUpdate: ' . $e->getMessage(), 3, _PS_ROOT_DIR . "log/retailcrm.log"); } } } } } - protected function getApiDeliveryTypes($intaroCrm) + protected function getApiDeliveryTypes() { $crmDeliveryTypes = array(); - try { - $deliveryTypes = $intaroCrm->deliveryTypesList(); - } - catch (\IntaroCrm\Exception\CurlException $e) { - error_log('deliveryTypesList: connection error', 3, "intarocrm.log"); - } - catch (\IntaroCrm\Exception\ApiException $e) { - error_log('deliveryTypesList: ' . $e->getMessage(), 3, "intarocrm.log"); - } + if (!empty($this->apiUrl) && !empty($this->apiKey)) { + try { + $deliveryTypes = $this->api->deliveryTypesList(); + } + catch (CurlException $e) { + error_log('deliveryTypesList: connection error', 3, _PS_ROOT_DIR . "log/retailcrm.log"); + } + catch (InvalidJsonException $e) { + error_log('deliveryTypesList: ' . $e->getMessage(), 3, _PS_ROOT_DIR . "log/retailcrm.log"); + } - if (!empty($deliveryTypes)) { - $crmDeliveryTypes[] = array(); - foreach ($deliveryTypes as $dType) { - $crmDeliveryTypes[] = array( - 'id_option' => $dType['code'], - 'name' => $dType['name'], - ); + if (!empty($deliveryTypes)) { + $crmDeliveryTypes[] = array(); + foreach ($deliveryTypes as $dType) { + $crmDeliveryTypes[] = array( + 'id_option' => $dType['code'], + 'name' => $dType['name'], + ); + } } } @@ -427,12 +440,12 @@ class IntaroCRM extends Module } - protected function getDeliveryTypes($default_lang, $intaroCrm) + protected function getDeliveryTypes() { $deliveryTypes = array(); $carriers = Carrier::getCarriers( - $default_lang, true, false, false, + $this->default_lang, true, false, false, null, PS_CARRIERS_AND_CARRIER_MODULES_NEED_RANGE ); @@ -441,10 +454,10 @@ class IntaroCRM extends Module $deliveryTypes[] = array( 'type' => 'select', 'label' => $carrier['name'], - 'name' => 'INTAROCRM_API_DELIVERY[' . $carrier['id_carrier'] . ']', + 'name' => 'RETAILCRM_API_DELIVERY[' . $carrier['id_carrier'] . ']', 'required' => false, 'options' => array( - 'query' => $this->getApiDeliveryTypes($intaroCrm), + 'query' => $this->getApiDeliveryTypes(), 'id' => 'id_option', 'name' => 'name' ) @@ -455,37 +468,39 @@ class IntaroCRM extends Module return $deliveryTypes; } - protected function getApiStatuses($intaroCrm) + protected function getApiStatuses() { $crmStatusTypes = array(); - try { - $statusTypes = $intaroCrm->orderStatusesList(); - } - catch (\IntaroCrm\Exception\CurlException $e) { - error_log('statusTypesList: connection error', 3, "intarocrm.log"); - } - catch (\IntaroCrm\Exception\ApiException $e) { - error_log('statusTypesList: ' . $e->getMessage(), 3, "intarocrm.log"); - } + if (!empty($this->apiUrl) && !empty($this->apiKey)) { + try { + $statusTypes = $this->api->statusesList(); + } + catch (CurlException $e) { + error_log('statusTypesList: connection error', 3, _PS_ROOT_DIR . "log/retailcrm.log"); + } + catch (InvalidJsonException $e) { + error_log('statusTypesList: ' . $e->getMessage(), 3, _PS_ROOT_DIR . "log/retailcrm.log"); + } - if (!empty($statusTypes)) { - $crmStatusTypes[] = array(); - foreach ($statusTypes as $sType) { - $crmStatusTypes[] = array( - 'id_option' => $sType['code'], - 'name' => $sType['name'] - ); + if (!empty($statusTypes)) { + $crmStatusTypes[] = array(); + foreach ($statusTypes as $sType) { + $crmStatusTypes[] = array( + 'id_option' => $sType['code'], + 'name' => $sType['name'] + ); + } } } return $crmStatusTypes; } - protected function getStatusTypes($default_lang, $intaroCrm) + protected function getStatusTypes() { $statusTypes = array(); - $states = OrderState::getOrderStates($default_lang, true); + $states = OrderState::getOrderStates($this->default_lang, true); if (!empty($states)) { foreach ($states as $state) { @@ -493,10 +508,10 @@ class IntaroCRM extends Module $statusTypes[] = array( 'type' => 'select', 'label' => $state['name'], - 'name' => 'INTAROCRM_API_STATUS[' . $state['id_order_state'] . ']', + 'name' => 'RETAILCRM_API_STATUS[' . $state['id_order_state'] . ']', 'required' => false, 'options' => array( - 'query' => $this->getApiStatuses($intaroCrm), + 'query' => $this->getApiStatuses(), 'id' => 'id_option', 'name' => 'name' ) @@ -508,34 +523,36 @@ class IntaroCRM extends Module return $statusTypes; } - protected function getApiPaymentTypes($intaroCrm) + protected function getApiPaymentTypes() { $crmPaymentTypes = array(); - try { - $paymentTypes = $intaroCrm->paymentTypesList(); - } - catch (\IntaroCrm\Exception\CurlException $e) { - error_log('paymentTypesList: connection error', 3, "intarocrm.log"); - } - catch (\IntaroCrm\Exception\ApiException $e) { - error_log('paymentTypesList: ' . $e->getMessage(), 3, "intarocrm.log"); - } + if (!empty($this->apiUrl) && !empty($this->apiKey)) { + try { + $paymentTypes = $this->api->paymentTypesList(); + } + catch (CurlException $e) { + error_log('paymentTypesList: connection error', 3, _PS_ROOT_DIR . "log/retailcrm.log"); + } + catch (InvalidJsonException $e) { + error_log('paymentTypesList: ' . $e->getMessage(), 3, _PS_ROOT_DIR . "log/retailcrm.log"); + } - if (!empty($paymentTypes)) { - $crmPaymentTypes[] = array(); - foreach ($paymentTypes as $pType) { - $crmPaymentTypes[] = array( - 'id_option' => $pType['code'], - 'name' => $pType['name'] - ); + if (!empty($paymentTypes)) { + $crmPaymentTypes[] = array(); + foreach ($paymentTypes as $pType) { + $crmPaymentTypes[] = array( + 'id_option' => $pType['code'], + 'name' => $pType['name'] + ); + } } } return $crmPaymentTypes; } - protected function getPaymentTypes($intaroCrm) + protected function getPaymentTypes() { $payments = $this->getSystemPaymentModules(); $paymentTypes = array(); @@ -545,10 +562,10 @@ class IntaroCRM extends Module $paymentTypes[] = array( 'type' => 'select', 'label' => $payment['name'], - 'name' => 'INTAROCRM_API_PAYMENT[' . $payment['code'] . ']', + 'name' => 'RETAILCRM_API_PAYMENT[' . $payment['code'] . ']', 'required' => false, 'options' => array( - 'query' => $this->getApiPaymentTypes($intaroCrm), + 'query' => $this->getApiPaymentTypes(), 'id' => 'id_option', 'name' => 'name' ) @@ -647,7 +664,7 @@ class IntaroCRM extends Module $addressFields[] = array( 'type' => 'select', 'label' => $this->l((string)$a), - 'name' => 'INTAROCRM_API_ADDR[' . $idx . ']', + 'name' => 'RETAILCRM_API_ADDR[' . $idx . ']', 'required' => false, 'options' => array( 'query' => array( @@ -775,16 +792,18 @@ class IntaroCRM extends Module $crewrite = Category::getLinkRewrite($product['id_category_default'], $id_lang); $url = $link->getProductLink($product['id_product'], $product['link_rewrite'], $crewrite); $version = substr(_PS_VERSION_, 0, 3); + if ($version == "1.3") - $available_for_order = $product['active'] && $product['quantity']; + $available_for_order = $product['active'] && $product['quantity']; else { $prod = new Product($product['id_product']); $available_for_order = $product['active'] && $product['available_for_order'] && $prod->checkQty(1); } + $items[] = array('id_product' => $product['id_product'], 'available_for_order' => $available_for_order, - 'price' => $product['price'], - 'purchase_price' => $product['wholesale_price'], + 'price' => round($product['price'],2), + 'purchase_price' => round($product['wholesale_price'], 2), 'name' => htmlspecialchars(strip_tags($product['name'])), 'article' => htmlspecialchars($product['reference']), 'id_category_default' => $category, @@ -813,7 +832,7 @@ class IntaroCRM extends Module /* * get last sync date */ - $lastSync = Configuration::get('INTAROCRM_LAST_SYNC'); + $lastSync = Configuration::get('RETAILCRM_LAST_SYNC'); $startFrom = ($lastSync === false) ? null : $lastSync; $endTime = date('Y-m-d H:i:s'); @@ -826,18 +845,18 @@ class IntaroCRM extends Module */ do { try { - $this->response = $this->intaroCRM->orderHistory( + $this->response = $this->api->ordersHistory( $startDate = $startFrom, $endDate = $endTime, $limit = 250, $offset = $counter ); $data = array_merge($data, $this->response); $counter += 250; } - catch (\IntaroCrm\Exception\CurlException $e) { - error_log('orderHistory: connection error', 3, "intarocrm.log"); + catch (CurlException $e) { + error_log('orderHistory: connection error', 3, _PS_ROOT_DIR . "log/retailcrm.log"); } - catch (\IntaroCrm\Exception\ApiException $e) { - error_log('orderHistory: ' . $e->getMessage(), 3, "intarocrm.log"); + catch (InvalidJsonException $e) { + error_log('orderHistory: ' . $e->getMessage(), 3, _PS_ROOT_DIR . "log/retailcrm.log"); } } while (!empty($response)); @@ -852,9 +871,9 @@ class IntaroCRM extends Module */ $this->customer = new Customer(); - $statuses = array_flip((array)json_decode(Configuration::get('INTAROCRM_API_STATUS'))); - $deliveries = array_flip((array)json_decode(Configuration::get('INTAROCRM_API_DELIVERY'))); - $payments = array_flip((array)json_decode(Configuration::get('INTAROCRM_API_PAYMENT'))); + $statuses = array_flip((array)json_decode(Configuration::get('RETAILCRM_API_STATUS'))); + $deliveries = array_flip((array)json_decode(Configuration::get('RETAILCRM_API_DELIVERY'))); + $payments = array_flip((array)json_decode(Configuration::get('RETAILCRM_API_PAYMENT'))); foreach ($data as $order) { if (!array_key_exists('externalId', $order)) { @@ -1048,15 +1067,15 @@ class IntaroCRM extends Module Db::getInstance()->execute(rtrim($query, ',')); try { - $this->intaroCRM->customerFixExternalIds($this->customerFix); - $this->intaroCRM->orderFixExternalIds($this->orderFix); + $this->api->customersFixExternalIds($this->customerFix); + $this->api->ordesrFixExternalIds($this->orderFix); } - catch (\IntaroCrm\Exception\CurlException $e) { - error_log('fixExternalId: connection error', 3, "intarocrm.log"); + catch (CurlException $e) { + error_log('fixExternalId: connection error', 3, _PS_ROOT_DIR . "log/retailcrm.log"); continue; } - catch (\IntaroCrm\Exception\ApiException $e) { - error_log('fixExternalId: ' . $e->getMessage(), 3, "intarocrm.log"); + catch (InvalidJsonException $e) { + error_log('fixExternalId: ' . $e->getMessage(), 3, _PS_ROOT_DIR . "log/retailcrm.log"); continue; } @@ -1244,15 +1263,15 @@ class IntaroCRM extends Module */ try { Configuration::updateValue( - 'INTAROCRM_LAST_SYNC', - date_format($this->intaroCRM->getGeneratedAt(), 'Y-m-d H:i:s') + 'RETAILCRM_LAST_SYNC', + date_format($this->api->getGeneratedAt(), 'Y-m-d H:i:s') ); } - catch (\IntaroCrm\Exception\CurlException $e) { - error_log('getLastSync: connection error', 3, "intarocrm.log"); + catch (CurlException $e) { + error_log('getLastSync: connection error', 3, _PS_ROOT_DIR . "log/retailcrm.log"); } - catch (\IntaroCrm\Exception\ApiException $e) { - error_log('getLastSync: ' . $e->getMessage(), 3, "intarocrm.log"); + catch (InvalidJsonException $e) { + error_log('getLastSync: ' . $e->getMessage(), 3, _PS_ROOT_DIR . "log/retailcrm.log"); } return count($data) . " records was synced"; diff --git a/intarocrm/sync.php b/retailcrm/sync.php similarity index 71% rename from intarocrm/sync.php rename to retailcrm/sync.php index bab4cc3..11e32a4 100644 --- a/intarocrm/sync.php +++ b/retailcrm/sync.php @@ -1,9 +1,9 @@ orderHistory(); diff --git a/intarocrm/translations/ru.php b/retailcrm/translations/ru.php similarity index 100% rename from intarocrm/translations/ru.php rename to retailcrm/translations/ru.php From 956414d08aa63b8049d2b54f5a1cd2976be20e37 Mon Sep 17 00:00:00 2001 From: Alex Lushpai Date: Mon, 20 Jul 2015 18:02:55 +0300 Subject: [PATCH 02/17] update readme --- README.md | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 7e59627..ac5580b 100644 --- a/README.md +++ b/README.md @@ -20,16 +20,10 @@ Installation git clone git@github.com:/intarocrm/prestashop-module.git ``` -#### Install Rest API Client. - -``` -cd prestashop-module -composer update -``` - #### Create .zip file. ``` -zip -r intarocrm.zip intarocrm +cd prestashop-module +zip -r retailcrm.zip retailcrm ``` #### Install via Admin interface. From 22ba5c906a7edd75f955de495b0d7b9a0dd23558 Mon Sep 17 00:00:00 2001 From: Alex Lushpai Date: Tue, 21 Jul 2015 15:21:12 +0300 Subject: [PATCH 03/17] restruct & custom --- CHANGELOG.md | 13 + README.md | 25 +- docs/images/add.png | Bin 0 -> 15633 bytes retailcrm/classes/References.php | 210 +++++++++++ retailcrm/config_ru.xml | 13 + retailcrm/{index.php => job_icml.php} | 0 retailcrm/{sync.php => job_sync.php} | 0 retailcrm/retailcrm.php | 515 +++++++------------------- retailcrm/translations/ru.php | 77 ++-- 9 files changed, 434 insertions(+), 419 deletions(-) create mode 100644 CHANGELOG.md create mode 100644 docs/images/add.png create mode 100644 retailcrm/classes/References.php create mode 100644 retailcrm/config_ru.xml rename retailcrm/{index.php => job_icml.php} (100%) rename retailcrm/{sync.php => job_sync.php} (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e98883b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +Changelog +========= + +v0.2 + +* Ребрендинг, intarocrm заменено на retailcrm +* Обновлена библиотека api-client-php +* Сборка через composer больше надоступна, все необходимые файлы включены в основноую поставку +* Добавлен механизм кастомизации +* Добавлены переводы +* Генерация ICML перенесена с контроллера на cron для более эффективного использования ресурсов +* Устранен раяд ошибок + diff --git a/README.md b/README.md index ac5580b..ca4554d 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,31 @@ Prestashop module -============= +================= -Prestashop module for interaction with [IntaroCRM](http://www.intarocrm.com) through [REST API](http://docs.intarocrm.ru/rest-api/). +Модуль интеграции CMS Prestashop c [RetailCRM](http://www.retailcrm.com) -Module allows: +Модуль позволяет: -* Send to IntaroCRM new orders -* Configure relations between dictionaries of IntaroCRM and Prestashop (statuses, payments, delivery types and etc) -* Generate [ICML](http://docs.intarocrm.ru/index.php?n=Пользователи.ФорматICML) (IntaroCRM Markup Language) for catalog loading by IntaroCRM +* Экспортировать в CRM заказы +* Синхронизировать справочники (способы доставки и оплаты, статусы заказов и т.п.) +* Выгружать каталог товаров в формате [ICML](http://retailcrm.ru/docs/Разработчики/ФорматICML) (IntaroCRM Markup Language) -Installation +Установка ------------- -### 1. Manual installation +### 1. Ручная установка -#### Clone module. +#### Скопируйте модуль ``` git clone git@github.com:/intarocrm/prestashop-module.git ``` -#### Create .zip file. +#### Создайте загружаемый .zip архив. ``` cd prestashop-module zip -r retailcrm.zip retailcrm ``` -#### Install via Admin interface. +#### Установите через административный интерфейс управления модулями. - -Go to Modules -> Add module. After that upload your zipped module and activate it. +![Установка модуля](/docs/images/add.png) diff --git a/docs/images/add.png b/docs/images/add.png new file mode 100644 index 0000000000000000000000000000000000000000..68bfb2a147ec0da49cd689e171dc764669c7a4cf GIT binary patch literal 15633 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 = $client; + $this->default_lang = (int) Configuration::get('PS_LANG_DEFAULT'); + $this->default_currency = (int) Configuration::get('PS_CURRENCY_DEFAULT'); + $this->default_country = (int) Configuration::get('PS_COUNTRY_DEFAULT'); + } + + public function getDeliveryTypes() + { + $deliveryTypes = array(); + + $carriers = Carrier::getCarriers( + $this->default_lang, true, false, false, + null, PS_CARRIERS_AND_CARRIER_MODULES_NEED_RANGE + ); + + if (!empty($carriers)) { + foreach ($carriers as $carrier) { + $deliveryTypes[] = array( + 'type' => 'select', + 'label' => $carrier['name'], + 'name' => 'RETAILCRM_API_DELIVERY[' . $carrier['id_carrier'] . ']', + 'required' => false, + 'options' => array( + 'query' => $this->getApiDeliveryTypes(), + 'id' => 'id_option', + 'name' => 'name' + ) + ); + } + } + + return $deliveryTypes; + } + + public function getStatuses() + { + $statusTypes = array(); + $states = array_merge( + OrderState::getOrderStates($this->default_lang, true), + OrderReturnState::getOrderReturnStates($this->default_lang, true) + ); + + if (!empty($states)) { + foreach ($states as $state) { + if ($state['name'] != ' ') { + $statusTypes[] = array( + 'type' => 'select', + 'label' => $state['name'], + 'name' => 'RETAILCRM_API_STATUS[' . $state['id_order_state'] . ']', + 'required' => false, + 'options' => array( + 'query' => $this->getApiStatuses(), + 'id' => 'id_option', + 'name' => 'name' + ) + ); + } + } + } + + return $statusTypes; + } + + public function getPaymentTypes() + { + $payments = $this->getSystemPaymentModules(); + $paymentTypes = array(); + + if (!empty($payments)) { + foreach ($payments as $payment) { + $paymentTypes[] = array( + 'type' => 'select', + 'label' => $payment['name'], + 'name' => 'RETAILCRM_API_PAYMENT[' . $payment['code'] . ']', + 'required' => false, + 'options' => array( + 'query' => $this->getApiPaymentTypes(), + 'id' => 'id_option', + 'name' => 'name' + ) + ); + } + } + + return $paymentTypes; + } + + protected function getSystemPaymentModules() + { + $shop_id = Context::getContext()->shop->id; + + /* Get all modules then select only payment ones */ + $modules = Module::getModulesOnDisk(true); + + foreach ($modules as $module) { + if ($module->tab == 'payments_gateways') { + if ($module->id) { + if (!get_class($module) == 'SimpleXMLElement') $module->country = array(); + $countries = DB::getInstance()->executeS('SELECT id_country FROM ' . _DB_PREFIX_ . 'module_country WHERE id_module = ' . (int) $module->id . ' AND `id_shop`=' . (int) $shop_id); + foreach ($countries as $country) $module->country[] = $country['id_country']; + if (!get_class($module) == 'SimpleXMLElement') $module->currency = array(); + $currencies = DB::getInstance()->executeS('SELECT id_currency FROM ' . _DB_PREFIX_ . 'module_currency WHERE id_module = ' . (int) $module->id . ' AND `id_shop`=' .(int) $shop_id); + foreach ($currencies as $currency) $module->currency[] = $currency['id_currency']; + if (!get_class($module) == 'SimpleXMLElement') $module->group = array(); + $groups = DB::getInstance()->executeS('SELECT id_group FROM ' . _DB_PREFIX_ . 'module_group WHERE id_module = ' . (int) $module->id . ' AND `id_shop`=' . (int) $shop_id); + foreach ($groups as $group) $module->group[] = $group['id_group']; + } else { + $module->country = null; + $module->currency = null; + $module->group = null; + } + + if ($module->active != 0) { + $this->payment_modules[] = array( + 'id' => $module->id, + 'code' => $module->name, + 'name' => $module->displayName + ); + } + } + } + + return $this->payment_modules; + } + + protected function getApiDeliveryTypes() + { + $crmDeliveryTypes = array(); + + try { + $request = $this->api->deliveryTypesList(); + if ($request->isSuccessful()) { + $crmDeliveryTypes[] = array(); + foreach ($request->deliveryTypes as $dType) { + $crmDeliveryTypes[] = array( + 'id_option' => $dType['code'], + 'name' => $dType['name'], + ); + } + } + } + catch (CurlException $e) { + error_log('deliveryTypesList: connection error', 3, _PS_ROOT_DIR . "log/retailcrm.log"); + } + catch (InvalidJsonException $e) { + error_log('deliveryTypesList: ' . $e->getMessage(), 3, _PS_ROOT_DIR . "log/retailcrm.log"); + } + + return $crmDeliveryTypes; + + } + + protected function getApiStatuses() + { + $crmStatusTypes = array(); + + try { + $request = $this->api->statusesList(); + if ($request->isSuccessful()) { + $crmStatusTypes[] = array(); + foreach ($request->statuses as $sType) { + $crmStatusTypes[] = array( + 'id_option' => $sType['code'], + 'name' => $sType['name'] + ); + } + } + } + catch (CurlException $e) { + error_log('statusTypesList: connection error', 3, _PS_ROOT_DIR . "log/retailcrm.log"); + } + catch (InvalidJsonException $e) { + error_log('statusTypesList: ' . $e->getMessage(), 3, _PS_ROOT_DIR . "log/retailcrm.log"); + } + + return $crmStatusTypes; + } + + protected function getApiPaymentTypes() + { + $crmPaymentTypes = array(); + + try { + $request = $this->api->paymentTypesList(); + if ($request->isSuccessful()) { + $crmPaymentTypes[] = array(); + foreach ($request->paymentTypes as $pType) { + $crmPaymentTypes[] = array( + 'id_option' => $pType['code'], + 'name' => $pType['name'] + ); + } + } + } + catch (CurlException $e) { + error_log('paymentTypesList: connection error', 3, _PS_ROOT_DIR . "log/retailcrm.log"); + } + catch (InvalidJsonException $e) { + error_log('paymentTypesList: ' . $e->getMessage(), 3, _PS_ROOT_DIR . "log/retailcrm.log"); + } + + return $crmPaymentTypes; + } +} diff --git a/retailcrm/config_ru.xml b/retailcrm/config_ru.xml new file mode 100644 index 0000000..754c073 --- /dev/null +++ b/retailcrm/config_ru.xml @@ -0,0 +1,13 @@ + + + retailcrm + + + + + + + 1 + 1 + + diff --git a/retailcrm/index.php b/retailcrm/job_icml.php similarity index 100% rename from retailcrm/index.php rename to retailcrm/job_icml.php diff --git a/retailcrm/sync.php b/retailcrm/job_sync.php similarity index 100% rename from retailcrm/sync.php rename to retailcrm/job_sync.php diff --git a/retailcrm/retailcrm.php b/retailcrm/retailcrm.php index d4eeae3..cd6ee62 100644 --- a/retailcrm/retailcrm.php +++ b/retailcrm/retailcrm.php @@ -4,6 +4,12 @@ require 'classes/Retailcrm.php'; require 'classes/Service.php'; require 'classes/Icml.php'; +if (file_exists('classes/ReferencesCustom.php')) { + require 'classes/ReferencesCustom.php'; +} else { + require 'classes/References.php'; +} + if (!defined('_PS_VERSION_')) { exit; } @@ -31,9 +37,9 @@ class RetailCRM extends Module ); } - $this->default_lang = (int)Configuration::get('PS_LANG_DEFAULT'); - $this->default_currency = (int)Configuration::get('PS_CURRENCY_DEFAULT'); - $this->default_country = (int)Configuration::get('PS_COUNTRY_DEFAULT'); + $this->default_lang = (int) Configuration::get('PS_LANG_DEFAULT'); + $this->default_currency = (int) Configuration::get('PS_CURRENCY_DEFAULT'); + $this->default_country = (int) Configuration::get('PS_COUNTRY_DEFAULT'); $this->response = array(); $this->customerFix = array(); @@ -44,6 +50,8 @@ class RetailCRM extends Module $this->customer = null; + $this->ref = new References($this->api); + parent::__construct(); } @@ -78,9 +86,9 @@ class RetailCRM extends Module $token = Configuration::get('RETAILCRM_API_TOKEN'); if (!$address || $address == '') { - $output .= $this->displayError( $this->l('Invalid crm address') ); + $output .= $this->displayError( $this->l('Invalid or empty crm address') ); } elseif (!$token || $token == '') { - $output .= $this->displayError( $this->l('Invalid crm api token') ); + $output .= $this->displayError( $this->l('Invalid or empty crm api token') ); } else { $output .= $this->displayConfirmation( $this->l('Timezone settings must be identical to both of your crm and shop') . @@ -162,7 +170,7 @@ class RetailCRM extends Module 'legend' => array( 'title' => $this->l('Delivery'), ), - 'input' => $this->getDeliveryTypes(), + 'input' => $this->ref->getDeliveryTypes(), ); /* @@ -172,7 +180,7 @@ class RetailCRM extends Module 'legend' => array( 'title' => $this->l('Order statuses'), ), - 'input' => $this->getStatusTypes(), + 'input' => $this->ref->getStatuses(), ); /* @@ -182,7 +190,7 @@ class RetailCRM extends Module 'legend' => array( 'title' => $this->l('Payment types'), ), - 'input' => $this->getPaymentTypes(), + 'input' => $this->ref->getPaymentTypes(), ); } @@ -295,7 +303,7 @@ class RetailCRM extends Module public function hookActionOrderStatusPostUpdate($params) { $address_id = Address::getFirstCustomerAddressId($params['cart']->id_customer); - $sql = 'SELECT * FROM '._DB_PREFIX_.'address WHERE id_address='.(int)$address_id; + $sql = 'SELECT * FROM '._DB_PREFIX_.'address WHERE id_address='.(int) $address_id; $address = Db::getInstance()->ExecuteS($sql); $address = $address[0]; $delivery = json_decode(Configuration::get('RETAILCRM_API_DELIVERY')); @@ -410,349 +418,11 @@ class RetailCRM extends Module } } - protected function getApiDeliveryTypes() - { - $crmDeliveryTypes = array(); - - if (!empty($this->apiUrl) && !empty($this->apiKey)) { - try { - $deliveryTypes = $this->api->deliveryTypesList(); - } - catch (CurlException $e) { - error_log('deliveryTypesList: connection error', 3, _PS_ROOT_DIR . "log/retailcrm.log"); - } - catch (InvalidJsonException $e) { - error_log('deliveryTypesList: ' . $e->getMessage(), 3, _PS_ROOT_DIR . "log/retailcrm.log"); - } - - if (!empty($deliveryTypes)) { - $crmDeliveryTypes[] = array(); - foreach ($deliveryTypes as $dType) { - $crmDeliveryTypes[] = array( - 'id_option' => $dType['code'], - 'name' => $dType['name'], - ); - } - } - } - - return $crmDeliveryTypes; - - } - - protected function getDeliveryTypes() - { - $deliveryTypes = array(); - - $carriers = Carrier::getCarriers( - $this->default_lang, true, false, false, - null, PS_CARRIERS_AND_CARRIER_MODULES_NEED_RANGE - ); - - if (!empty($carriers)) { - foreach ($carriers as $carrier) { - $deliveryTypes[] = array( - 'type' => 'select', - 'label' => $carrier['name'], - 'name' => 'RETAILCRM_API_DELIVERY[' . $carrier['id_carrier'] . ']', - 'required' => false, - 'options' => array( - 'query' => $this->getApiDeliveryTypes(), - 'id' => 'id_option', - 'name' => 'name' - ) - ); - } - } - - return $deliveryTypes; - } - - protected function getApiStatuses() - { - $crmStatusTypes = array(); - - if (!empty($this->apiUrl) && !empty($this->apiKey)) { - try { - $statusTypes = $this->api->statusesList(); - } - catch (CurlException $e) { - error_log('statusTypesList: connection error', 3, _PS_ROOT_DIR . "log/retailcrm.log"); - } - catch (InvalidJsonException $e) { - error_log('statusTypesList: ' . $e->getMessage(), 3, _PS_ROOT_DIR . "log/retailcrm.log"); - } - - if (!empty($statusTypes)) { - $crmStatusTypes[] = array(); - foreach ($statusTypes as $sType) { - $crmStatusTypes[] = array( - 'id_option' => $sType['code'], - 'name' => $sType['name'] - ); - } - } - } - - return $crmStatusTypes; - } - - protected function getStatusTypes() - { - $statusTypes = array(); - $states = OrderState::getOrderStates($this->default_lang, true); - - if (!empty($states)) { - foreach ($states as $state) { - if ($state['name'] != ' ') { - $statusTypes[] = array( - 'type' => 'select', - 'label' => $state['name'], - 'name' => 'RETAILCRM_API_STATUS[' . $state['id_order_state'] . ']', - 'required' => false, - 'options' => array( - 'query' => $this->getApiStatuses(), - 'id' => 'id_option', - 'name' => 'name' - ) - ); - } - } - } - - return $statusTypes; - } - - protected function getApiPaymentTypes() - { - $crmPaymentTypes = array(); - - if (!empty($this->apiUrl) && !empty($this->apiKey)) { - try { - $paymentTypes = $this->api->paymentTypesList(); - } - catch (CurlException $e) { - error_log('paymentTypesList: connection error', 3, _PS_ROOT_DIR . "log/retailcrm.log"); - } - catch (InvalidJsonException $e) { - error_log('paymentTypesList: ' . $e->getMessage(), 3, _PS_ROOT_DIR . "log/retailcrm.log"); - } - - if (!empty($paymentTypes)) { - $crmPaymentTypes[] = array(); - foreach ($paymentTypes as $pType) { - $crmPaymentTypes[] = array( - 'id_option' => $pType['code'], - 'name' => $pType['name'] - ); - } - } - } - - return $crmPaymentTypes; - } - - protected function getPaymentTypes() - { - $payments = $this->getSystemPaymentModules(); - $paymentTypes = array(); - - if (!empty($payments)) { - foreach ($payments as $payment) { - $paymentTypes[] = array( - 'type' => 'select', - 'label' => $payment['name'], - 'name' => 'RETAILCRM_API_PAYMENT[' . $payment['code'] . ']', - 'required' => false, - 'options' => array( - 'query' => $this->getApiPaymentTypes(), - 'id' => 'id_option', - 'name' => 'name' - ) - ); - } - } - - return $paymentTypes; - } - - protected function getSystemPaymentModules() - { - $shop_id = Context::getContext()->shop->id; - - /* Get all modules then select only payment ones */ - $modules = Module::getModulesOnDisk(true); - /* - * Hack for knivesland - */ - if (Module::getInstanceByName('advancedcheckout') === false) { - foreach ($modules as $module) { - if ($module->tab == 'payments_gateways') - { - if ($module->id) - { - if (!get_class($module) == 'SimpleXMLElement') - $module->country = array(); - $countries = DB::getInstance()->executeS(' - SELECT id_country - FROM '._DB_PREFIX_.'module_country - WHERE id_module = '.(int)$module->id.' AND `id_shop`='.(int)$shop_id - ); - foreach ($countries as $country) - $module->country[] = $country['id_country']; - - if (!get_class($module) == 'SimpleXMLElement') - $module->currency = array(); - $currencies = DB::getInstance()->executeS(' - SELECT id_currency - FROM '._DB_PREFIX_.'module_currency - WHERE id_module = '.(int)$module->id.' AND `id_shop`='.(int)$shop_id - ); - foreach ($currencies as $currency) - $module->currency[] = $currency['id_currency']; - - if (!get_class($module) == 'SimpleXMLElement') - $module->group = array(); - $groups = DB::getInstance()->executeS(' - SELECT id_group - FROM '._DB_PREFIX_.'module_group - WHERE id_module = '.(int)$module->id.' AND `id_shop`='.(int)$shop_id - ); - foreach ($groups as $group) - $module->group[] = $group['id_group']; - } - else - { - $module->country = null; - $module->currency = null; - $module->group = null; - } - - if ($module->active != 0) { - $this->payment_modules[] = array( - 'id' => $module->id, - 'code' => $module->name, - 'name' => $module->displayName - ); - } - - } - } - } else { - require_once(dirname(__FILE__) . '/../advancedcheckout/classes/Payment.php'); - $modules = Payment::getPaymentMethods(); - foreach ($modules as $module) { - $this->payment_modules[] = array( - 'id' => $module['id_payment'], - 'code' => $module['name'], - 'name' => $module['name'] - ); - } - } - - return $this->payment_modules; - } - - protected function getAddressFields() - { - $addressFields = array(); - $address = explode(' ', str_replace("\n", ' ', AddressFormat::getAddressCountryFormat($this->context->country->id))); - - if (!empty($address)) { - foreach ($address as $idx => $a) { - if (!strpos($a, ':')) { - $addressFields[] = array( - 'type' => 'select', - 'label' => $this->l((string)$a), - 'name' => 'RETAILCRM_API_ADDR[' . $idx . ']', - 'required' => false, - 'options' => array( - 'query' => array( - array( - 'name' => '', - 'id_option' => '' - ), - array( - 'name' => $this->l('FIRST_NAME'), - 'id_option' => 'first_name' - ), - array( - 'name' => $this->l('LAST_NAME'), - 'id_option' => 'last_name' - ), - array( - 'name' => $this->l('PHONE'), - 'id_option' => 'phone' - ), - array( - 'name' => $this->l('EMAIL'), - 'id_option' => 'email' - ), - array( - 'name' => $this->l('ADDRESS'), - 'id_option' => 'address' - ), - array( - 'name' => $this->l('COUNTRY'), - 'id_option' => 'country' - ), - array( - 'name' => $this->l('REGION'), - 'id_option' => 'region' - ), - array( - 'name' => $this->l('CITY'), - 'id_option' => 'city' - ), - array( - 'name' => $this->l('ZIP'), - 'id_option' => 'index' - ), - array( - 'name' => $this->l('STREET'), - 'id_option' => 'street' - ), - array( - 'name' => $this->l('BUILDING'), - 'id_option' => 'building' - ), - array( - 'name' => $this->l('FLAT'), - 'id_option' => 'flat' - ), - array( - 'name' => $this->l('INTERCOMCODE'), - 'id_option' => 'intercomcode' - ), - array( - 'name' => $this->l('FLOOR'), - 'id_option' => 'floor' - ), - array( - 'name' => $this->l('BLOCK'), - 'id_option' => 'block' - ), - array( - 'name' => $this->l('HOUSE'), - 'ID' => 'house' - ) - ), - 'id' => 'id_option', - 'name' => 'name' - ) - ); - } - } - } - - return $addressFields; - } - public function exportCatalog() { global $smarty; $shop_url = (Configuration::get('PS_SSL_ENABLED') ? _PS_BASE_URL_SSL_ : _PS_BASE_URL_); - $id_lang = (int)Configuration::get('PS_LANG_DEFAULT'); + $id_lang = (int) Configuration::get('PS_LANG_DEFAULT'); $currency = new Currency(Configuration::get('PS_CURRENCY_DEFAULT')); if ($currency->iso_code == 'RUB') { $currency->iso_code = 'RUR'; @@ -952,9 +622,9 @@ class RetailCRM extends Module $cart->id_currency = $this->default_currency; $cart->id_lang = $this->default_lang; $cart->id_customer = $this->customer_id; - $cart->id_address_delivery = (int)$this->address_id; - $cart->id_address_invoice = (int)$this->address_id; - $cart->id_carrier = (int)$deliveries[$delivery]; + $cart->id_address_delivery = (int) $this->address_id; + $cart->id_address_invoice = (int) $this->address_id; + $cart->id_carrier = (int) $deliveries[$delivery]; $cart->add(); @@ -962,9 +632,9 @@ class RetailCRM extends Module if(!empty($order['items'])) { foreach ($order['items'] as $item) { $product = array(); - $product['id_product'] = (int)$item['offer']['externalId']; + $product['id_product'] = (int) $item['offer']['externalId']; $product['quantity'] = $item['quantity']; - $product['id_address_delivery'] = (int)$this->address_id; + $product['id_address_delivery'] = (int) $this->address_id; $products[] = $product; } } @@ -977,13 +647,13 @@ class RetailCRM extends Module */ $newOrder = new Order(); - $newOrder->id_address_delivery = (int)$this->address_id; - $newOrder->id_address_invoice = (int)$this->address_id; - $newOrder->id_cart = (int)$cart->id; + $newOrder->id_address_delivery = (int) $this->address_id; + $newOrder->id_address_invoice = (int) $this->address_id; + $newOrder->id_cart = (int) $cart->id; $newOrder->id_currency = $this->default_currency; $newOrder->id_lang = $this->default_lang; - $newOrder->id_customer = (int)$this->customer_id; - $newOrder->id_carrier = (int)$deliveries[$delivery]; + $newOrder->id_customer = (int) $this->customer_id; + $newOrder->id_carrier = (int) $deliveries[$delivery]; $newOrder->payment = $payments[$payment]; $newOrder->module = (Module::getInstanceByName('advancedcheckout') === false) ? $payments[$payment] @@ -999,7 +669,7 @@ class RetailCRM extends Module $newOrder->total_shipping_tax_incl = $order['deliveryCost']; $newOrder->total_shipping_tax_excl = $order['deliveryCost']; $newOrder->conversion_rate = 1.000000; - $newOrder->current_state = (int)$statuses[$state]; + $newOrder->current_state = (int) $statuses[$state]; $newOrder->delivery_date = $order['deliveryDate']; $newOrder->date_add = $order['createdAt']; $newOrder->date_upd = $order['createdAt']; @@ -1029,7 +699,7 @@ class RetailCRM extends Module */ $product_list = array(); foreach ($order['items'] as $item) { - $product = new Product((int)$item['offer']['externalId'], false, $this->default_lang); + $product = new Product((int) $item['offer']['externalId'], false, $this->default_lang); $qty = $item['quantity']; $product_list[] = array('product' =>$product, 'quantity' => $qty); } @@ -1046,14 +716,14 @@ class RetailCRM extends Module foreach ($product_list as $product) { $query .= '(' - .(int)$newOrder->id.', + .(int) $newOrder->id.', 0, '. $this->context->shop->id.', - '.(int)$product['product']->id.', + '.(int) $product['product']->id.', 0, '.implode('', array('\'', $product['product']->name, '\'')).', - '.(int)$product['quantity'].', - '.(int)$product['quantity'].', + '.(int) $product['quantity'].', + '.(int) $product['quantity'].', '.$product['product']->price.', '.implode('', array('\'', $product['product']->reference, '\'')).', '.$product['product']->price.', @@ -1087,7 +757,7 @@ class RetailCRM extends Module */ $toUpdate[] = $order['id']; if ($order['paymentType'] != null && $order['deliveryType'] != null && $order['status'] != null) { - $orderToUpdate = new Order((int)$order['externalId']); + $orderToUpdate = new Order((int) $order['externalId']); /* * check status @@ -1098,7 +768,7 @@ class RetailCRM extends Module Db::getInstance()->execute(' UPDATE `'._DB_PREFIX_.'orders` SET `current_state` = \''.$statuses[$stype].'\' - WHERE `id_order` = '.(int)$order['externalId']); + WHERE `id_order` = '.(int) $order['externalId']); } } @@ -1111,7 +781,7 @@ class RetailCRM extends Module Db::getInstance()->execute(' UPDATE `'._DB_PREFIX_.'orders` SET `id_carrier` = \''.$deliveries[$dtype].'\' - WHERE `id_order` = '.(int)$order['externalId']); + WHERE `id_order` = '.(int) $order['externalId']); Db::getInstance()->execute(' UPDATE `'._DB_PREFIX_.'order_carrier` SET `id_carrier` = \''.$deliveries[$dtype].'\' @@ -1128,7 +798,7 @@ class RetailCRM extends Module Db::getInstance()->execute(' UPDATE `'._DB_PREFIX_.'orders` SET `payment` = \''.$payments[$ptype].'\' - WHERE `id_order` = '.(int)$order['externalId']); + WHERE `id_order` = '.(int) $order['externalId']); Db::getInstance()->execute(' UPDATE `'._DB_PREFIX_.'order_payment` SET `payment_method` = \''.$payments[$ptype].'\' @@ -1180,7 +850,7 @@ class RetailCRM extends Module */ if (!empty($order['items'])) { foreach ($order['items'] as $key => $newItem) { - $product = new Product((int)$newItem['offer']['externalId'], false, $this->default_lang); + $product = new Product((int) $newItem['offer']['externalId'], false, $this->default_lang); $qty = $newItem['quantity']; $product_list[] = array('product' =>$product, 'quantity' => $qty); } @@ -1198,14 +868,14 @@ class RetailCRM extends Module foreach ($product_list as $product) { $query .= '(' - .(int)$orderToUpdate->id.', + .(int) $orderToUpdate->id.', 0, '. $this->context->shop->id.', - '.(int)$product['product']->id.', + '.(int) $product['product']->id.', 0, '.implode('', array('\'', $product['product']->name, '\'')).', - '.(int)$product['quantity'].', - '.(int)$product['quantity'].', + '.(int) $product['quantity'].', + '.(int) $product['quantity'].', '.$product['product']->price.', '.implode('', array('\'', $product['product']->reference, '\'')).', '.$product['product']->price.', @@ -1251,7 +921,7 @@ class RetailCRM extends Module `total_paid` = '.$orderTotal.', `total_paid_tax_incl` = '.$orderTotal.', `total_paid_tax_excl` = '.$orderTotal.' - WHERE `id_order` = '.(int)$order['externalId']); + WHERE `id_order` = '.(int) $order['externalId']); } } } @@ -1281,4 +951,101 @@ class RetailCRM extends Module } } + + public function getAddressFields() + { + $addressFields = array(); + $address = explode(' ', str_replace("\n", ' ', AddressFormat::getAddressCountryFormat($this->context->country->id))); + + if (!empty($address)) { + foreach ($address as $idx => $a) { + if (!strpos($a, ':')) { + $a = preg_replace('/_/', ' ', $a); + $a = preg_replace('/[\,\.]/', '', $a); + $addressFields[] = array( + 'type' => 'select', + 'label' => $this->l((string) ucfirst($a)), + 'name' => 'RETAILCRM_API_ADDR[' . $idx . ']', + 'required' => false, + 'options' => array( + 'query' => array( + array( + 'name' => '', + 'id_option' => '' + ), + array( + 'name' => $this->l('FIRST_NAME'), + 'id_option' => 'first_name' + ), + array( + 'name' => $this->l('LAST_NAME'), + 'id_option' => 'last_name' + ), + array( + 'name' => $this->l('PHONE'), + 'id_option' => 'phone' + ), + array( + 'name' => $this->l('EMAIL'), + 'id_option' => 'email' + ), + array( + 'name' => $this->l('ADDRESS'), + 'id_option' => 'address' + ), + array( + 'name' => $this->l('COUNTRY'), + 'id_option' => 'country' + ), + array( + 'name' => $this->l('REGION'), + 'id_option' => 'region' + ), + array( + 'name' => $this->l('CITY'), + 'id_option' => 'city' + ), + array( + 'name' => $this->l('ZIP'), + 'id_option' => 'index' + ), + array( + 'name' => $this->l('STREET'), + 'id_option' => 'street' + ), + array( + 'name' => $this->l('BUILDING'), + 'id_option' => 'building' + ), + array( + 'name' => $this->l('FLAT'), + 'id_option' => 'flat' + ), + array( + 'name' => $this->l('INTERCOMCODE'), + 'id_option' => 'intercomcode' + ), + array( + 'name' => $this->l('FLOOR'), + 'id_option' => 'floor' + ), + array( + 'name' => $this->l('BLOCK'), + 'id_option' => 'block' + ), + array( + 'name' => $this->l('HOUSE'), + 'ID' => 'house' + ) + ), + 'id' => 'id_option', + 'name' => 'name' + ) + ); + } + } + } + + return $addressFields; + } } diff --git a/retailcrm/translations/ru.php b/retailcrm/translations/ru.php index bc44b2e..901ffc1 100644 --- a/retailcrm/translations/ru.php +++ b/retailcrm/translations/ru.php @@ -2,35 +2,48 @@ global $_MODULE; $_MODULE = array(); -$_MODULE['<{intarocrm}prestashop>intarocrm_03c4d9465b9b3a7533d18cacc79c7fe4'] = 'IntaroCRM'; -$_MODULE['<{intarocrm}prestashop>intarocrm_5e695dc9fe273b7bc074e608113f4662'] = 'Модуль интеграции с IntaroCRM'; -$_MODULE['<{intarocrm}prestashop>intarocrm_876f23178c29dc2552c0b48bf23cd9bd'] = 'Вы уверены, что хотите удалить модуль?'; -$_MODULE['<{intarocrm}prestashop>intarocrm_5effd5157947e8ba4a08883f198b2e31'] = 'Неверный адрес CRM'; -$_MODULE['<{intarocrm}prestashop>intarocrm_576300f5b6faeb746bb6d034d98e7afd'] = 'Неверный API ключ'; -$_MODULE['<{intarocrm}prestashop>intarocrm_fba05687b61bc936d1a9a92371ba8bcf'] = 'Внимание! Часовой пояс в CRM должен совпадать с часовым поясом в магазине, настроки часового пояса CRM можно задать по адресу:'; -$_MODULE['<{intarocrm}prestashop>intarocrm_c888438d14855d7d96a2724ee9c306bd'] = 'Настройки обновлены'; -$_MODULE['<{intarocrm}prestashop>intarocrm_51af428aa0dcceb5230acb267ecb91c5'] = 'Настройка соединения'; -$_MODULE['<{intarocrm}prestashop>intarocrm_4cbd5dbeeef7392e50358b1bc00dd592'] = 'Адрес CRM'; -$_MODULE['<{intarocrm}prestashop>intarocrm_7f775042e08eddee6bbfd8fbe0add4a3'] = 'Ключ авторизации'; -$_MODULE['<{intarocrm}prestashop>intarocrm_c9cc8cce247e49bae79f15173ce97354'] = 'Сохранить'; -$_MODULE['<{intarocrm}prestashop>intarocrm_065ab3a28ca4f16f55f103adc7d0226f'] = 'Способы доставки'; -$_MODULE['<{intarocrm}prestashop>intarocrm_33af8066d3c83110d4bd897f687cedd2'] = 'Статусы заказов'; -$_MODULE['<{intarocrm}prestashop>intarocrm_bab959acc06bb03897b294fbb892be6b'] = 'Способы оплаты'; -$_MODULE['<{intarocrm}prestashop>intarocrm_dd7bf230fde8d4836917806aff6a6b27'] = 'Адрес'; -$_MODULE['<{intarocrm}prestashop>intarocrm_630f6dc397fe74e52d5189e2c80f282b'] = 'Вернуться к списку'; -$_MODULE['<{intarocrm}prestashop>intarocrm_5c1cf6cfec2dad86c8ca5286a0294516'] = 'Имя'; -$_MODULE['<{intarocrm}prestashop>intarocrm_c695cfe527a6fcd680114851b86b7555'] = 'Фамилия'; -$_MODULE['<{intarocrm}prestashop>intarocrm_f9dd946cc89c1f3b41a0edbe0f36931d'] = 'Телефон'; -$_MODULE['<{intarocrm}prestashop>intarocrm_61a649a33f2869e5e35fbb7aff3a80d9'] = 'Email'; -$_MODULE['<{intarocrm}prestashop>intarocrm_2664f03ac6b8bb9eee4287720e407db3'] = 'Адрес'; -$_MODULE['<{intarocrm}prestashop>intarocrm_6ddc09dc456001d9854e9fe670374eb2'] = 'Страна'; -$_MODULE['<{intarocrm}prestashop>intarocrm_69aede266809f89b89fe70681f6a129f'] = 'Область/Край/Республика'; -$_MODULE['<{intarocrm}prestashop>intarocrm_859214628431995197c0558f7b5f8ffc'] = 'Город'; -$_MODULE['<{intarocrm}prestashop>intarocrm_4348f938bbddd8475e967ccb47ecb234'] = 'Почтовый индекс'; -$_MODULE['<{intarocrm}prestashop>intarocrm_78fce82336bbbdca7f6da7564b8f9325'] = 'Улица'; -$_MODULE['<{intarocrm}prestashop>intarocrm_71a6834884666147c0334f0c40bc7295'] = 'Дом/Строение'; -$_MODULE['<{intarocrm}prestashop>intarocrm_f88a77e3d68d251c3dc4008c327b5a0c'] = 'Квартира'; -$_MODULE['<{intarocrm}prestashop>intarocrm_d977f846d110fcb7f71c6f97330c9d10'] = 'Код домофона'; -$_MODULE['<{intarocrm}prestashop>intarocrm_56c1e354d36beb85b0d881c5b2e24cbe'] = 'Этаж'; -$_MODULE['<{intarocrm}prestashop>intarocrm_4d34f53389ed7f28ca91fc31ea360a66'] = 'Корпус'; -$_MODULE['<{intarocrm}prestashop>intarocrm_49354b452ec305136a56fe7731834156'] = 'Дом/Строение'; +$_MODULE['<{retailcrm}prestashop>retailcrm_463dc31aa1a0b6e871b1a9fed8e9860a'] = 'RetailCRM'; +$_MODULE['<{retailcrm}prestashop>retailcrm_30de6237576b9a24f6fc599c22a35a4b'] = 'Модуль интеграции с RetailCRM'; +$_MODULE['<{retailcrm}prestashop>retailcrm_876f23178c29dc2552c0b48bf23cd9bd'] = 'Вы уверены, что хотите удалить модуль?'; +$_MODULE['<{retailcrm}prestashop>retailcrm_b9b2d9f66d0112f3aae7dbdbd4e22a43'] = 'Некорректный или пустой адрес CRM'; +$_MODULE['<{retailcrm}prestashop>retailcrm_942010ef43f3fec28741f62a0d9ff29c'] = 'Некорректный или пустой ключ CRM'; +$_MODULE['<{retailcrm}prestashop>retailcrm_fba05687b61bc936d1a9a92371ba8bcf'] = 'Внимание! Часовой пояс в CRM должен совпадать с часовым поясом в магазине, настроки часового пояса CRM можно задать по адресу:'; +$_MODULE['<{retailcrm}prestashop>retailcrm_5effd5157947e8ba4a08883f198b2e31'] = 'Неверный или пустой адрес CRM'; +$_MODULE['<{retailcrm}prestashop>retailcrm_576300f5b6faeb746bb6d034d98e7afd'] = 'Неверный или пустой API ключ'; +$_MODULE['<{retailcrm}prestashop>retailcrm_c888438d14855d7d96a2724ee9c306bd'] = 'Настройки обновлены'; +$_MODULE['<{retailcrm}prestashop>retailcrm_51af428aa0dcceb5230acb267ecb91c5'] = 'Настройка соединения'; +$_MODULE['<{retailcrm}prestashop>retailcrm_4cbd5dbeeef7392e50358b1bc00dd592'] = 'URL адрес CRM'; +$_MODULE['<{retailcrm}prestashop>retailcrm_7f775042e08eddee6bbfd8fbe0add4a3'] = 'API ключ'; +$_MODULE['<{retailcrm}prestashop>retailcrm_c9cc8cce247e49bae79f15173ce97354'] = 'Сохранить'; +$_MODULE['<{retailcrm}prestashop>retailcrm_065ab3a28ca4f16f55f103adc7d0226f'] = 'Способы доставки'; +$_MODULE['<{retailcrm}prestashop>retailcrm_33af8066d3c83110d4bd897f687cedd2'] = 'Статусы заказов'; +$_MODULE['<{retailcrm}prestashop>retailcrm_bab959acc06bb03897b294fbb892be6b'] = 'Способы оплаты'; +$_MODULE['<{retailcrm}prestashop>retailcrm_dd7bf230fde8d4836917806aff6a6b27'] = 'Адрес'; +$_MODULE['<{retailcrm}prestashop>retailcrm_630f6dc397fe74e52d5189e2c80f282b'] = 'Вернуться к списку'; +$_MODULE['<{retailcrm}prestashop>retailcrm_5c1cf6cfec2dad86c8ca5286a0294516'] = 'Имя'; +$_MODULE['<{retailcrm}prestashop>retailcrm_c695cfe527a6fcd680114851b86b7555'] = 'Фамилия'; +$_MODULE['<{retailcrm}prestashop>retailcrm_f9dd946cc89c1f3b41a0edbe0f36931d'] = 'Телефон'; +$_MODULE['<{retailcrm}prestashop>retailcrm_61a649a33f2869e5e35fbb7aff3a80d9'] = 'Email'; +$_MODULE['<{retailcrm}prestashop>retailcrm_2664f03ac6b8bb9eee4287720e407db3'] = 'Адрес'; +$_MODULE['<{retailcrm}prestashop>retailcrm_6ddc09dc456001d9854e9fe670374eb2'] = 'Страна'; +$_MODULE['<{retailcrm}prestashop>retailcrm_69aede266809f89b89fe70681f6a129f'] = 'Область/Край/Республика'; +$_MODULE['<{retailcrm}prestashop>retailcrm_859214628431995197c0558f7b5f8ffc'] = 'Город'; +$_MODULE['<{retailcrm}prestashop>retailcrm_4348f938bbddd8475e967ccb47ecb234'] = 'Почтовый индекс'; +$_MODULE['<{retailcrm}prestashop>retailcrm_78fce82336bbbdca7f6da7564b8f9325'] = 'Улица'; +$_MODULE['<{retailcrm}prestashop>retailcrm_71a6834884666147c0334f0c40bc7295'] = 'Дом/Строение'; +$_MODULE['<{retailcrm}prestashop>retailcrm_f88a77e3d68d251c3dc4008c327b5a0c'] = 'Квартира'; +$_MODULE['<{retailcrm}prestashop>retailcrm_d977f846d110fcb7f71c6f97330c9d10'] = 'Код домофона'; +$_MODULE['<{retailcrm}prestashop>retailcrm_56c1e354d36beb85b0d881c5b2e24cbe'] = 'Этаж'; +$_MODULE['<{retailcrm}prestashop>retailcrm_4d34f53389ed7f28ca91fc31ea360a66'] = 'Корпус'; +$_MODULE['<{retailcrm}prestashop>retailcrm_49354b452ec305136a56fe7731834156'] = 'Дом/Строение'; +$_MODULE['<{retailcrm}prestashop>retailcrm_04176f095283bc729f1e3926967e7034'] = 'Имя'; +$_MODULE['<{retailcrm}prestashop>retailcrm_dff4bf10409100d989495c6d5486035e'] = 'Фамилия'; +$_MODULE['<{retailcrm}prestashop>retailcrm_1c76cbfe21c6f44c1d1e59d54f3e4420'] = 'Компания'; +$_MODULE['<{retailcrm}prestashop>retailcrm_1aadcc03a9dbba84a3c5a5cbfde8a162'] = 'ИНН'; +$_MODULE['<{retailcrm}prestashop>retailcrm_93d03fe37ab3c6abc2a19dd8e41543bd'] = 'Адрес строка 1'; +$_MODULE['<{retailcrm}prestashop>retailcrm_22fcffe02ab9eda5b769387122f2ddce'] = 'Адрес строка 2'; +$_MODULE['<{retailcrm}prestashop>retailcrm_8bcdc441379cbf584638b0589a3f9adb'] = 'Почтовый индекс'; +$_MODULE['<{retailcrm}prestashop>retailcrm_69aede266809f89b89fe70681f6a129f'] = 'Область/Край/Республика'; +$_MODULE['<{retailcrm}prestashop>retailcrm_57d056ed0984166336b7879c2af3657f'] = 'Город'; +$_MODULE['<{retailcrm}prestashop>retailcrm_bcc254b55c4a1babdf1dcb82c207506b'] = 'Телефон'; +$_MODULE['<{retailcrm}prestashop>retailcrm_f0e1fc6f97d36cb80f29196e2662ffde'] = 'Мобильный телефон'; From c72cfb649d37a4a78b3caccd0a785450c32156c3 Mon Sep 17 00:00:00 2001 From: Alex Lushpai Date: Tue, 21 Jul 2015 15:41:30 +0300 Subject: [PATCH 04/17] docs pics --- README.md | 9 +++++++++ docs/images/ref.png | Bin 0 -> 53487 bytes docs/images/setup.png | Bin 0 -> 59635 bytes 3 files changed, 9 insertions(+) create mode 100644 docs/images/ref.png create mode 100644 docs/images/setup.png diff --git a/README.md b/README.md index ca4554d..246b2b2 100644 --- a/README.md +++ b/README.md @@ -29,3 +29,12 @@ zip -r retailcrm.zip retailcrm #### Установите через административный интерфейс управления модулями. ![Установка модуля](/docs/images/add.png) + +#### Перейдите к настройкам + +![Настройка модуля](/docs/images/setup.png) + +#### Введите адрес и API ключ вашей CRM и задайте соответствие справочников + +![Справочники](/docs/images/ref.png) + diff --git a/docs/images/ref.png b/docs/images/ref.png new file mode 100644 index 0000000000000000000000000000000000000000..c5621138f16c1cb8a15043eb761f0227d6fac3b3 GIT binary patch literal 53487 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 z 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 literal 0 HcmV?d00001 diff --git a/docs/images/setup.png b/docs/images/setup.png new file mode 100644 index 0000000000000000000000000000000000000000..18aa75ce17e785826228d94ec82368e315624e21 GIT binary patch literal 59635 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 jA`B%{L|Fb-htyrRctH}QOxLFb6g Date: Thu, 23 Jul 2015 00:32:39 +0300 Subject: [PATCH 05/17] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 246b2b2..37d7a8b 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Prestashop module Модуль позволяет: -* Экспортировать в CRM заказы +* Экспортировать в CRM данные о заказах и клиентах и получать обратно изменения по этим данным * Синхронизировать справочники (способы доставки и оплаты, статусы заказов и т.п.) * Выгружать каталог товаров в формате [ICML](http://retailcrm.ru/docs/Разработчики/ФорматICML) (IntaroCRM Markup Language) From 24717dd0f1228bdcc383bb4b6834ce058ad363e2 Mon Sep 17 00:00:00 2001 From: Alex Lushpai Date: Fri, 23 Oct 2015 17:41:05 +0300 Subject: [PATCH 06/17] new independent structure, override ability --- retailcrm/config.xml | 2 +- retailcrm/config_ru.xml | 6 +- retailcrm/export.tpl | 31 - retailcrm/job/icml.php | 21 + retailcrm/job/sync.php | 5 + retailcrm/job_icml.php | 9 - retailcrm/job_sync.php | 9 - retailcrm/lib/classes/Catalog.php | 98 ++ retailcrm/lib/classes/OrderHistory.php | 459 ++++++++++ retailcrm/{ => lib}/classes/References.php | 0 retailcrm/lib/custom/.gitkeep | 0 retailcrm/{classes => lib/vendor}/Icml.php | 0 .../{classes => lib/vendor}/Retailcrm.php | 0 retailcrm/{classes => lib/vendor}/Service.php | 0 retailcrm/retailcrm.php | 838 ++++-------------- 15 files changed, 743 insertions(+), 735 deletions(-) delete mode 100644 retailcrm/export.tpl create mode 100644 retailcrm/job/icml.php create mode 100644 retailcrm/job/sync.php delete mode 100644 retailcrm/job_icml.php delete mode 100644 retailcrm/job_sync.php create mode 100644 retailcrm/lib/classes/Catalog.php create mode 100644 retailcrm/lib/classes/OrderHistory.php rename retailcrm/{ => lib}/classes/References.php (100%) create mode 100644 retailcrm/lib/custom/.gitkeep rename retailcrm/{classes => lib/vendor}/Icml.php (100%) rename retailcrm/{classes => lib/vendor}/Retailcrm.php (100%) rename retailcrm/{classes => lib/vendor}/Service.php (100%) diff --git a/retailcrm/config.xml b/retailcrm/config.xml index c3bb2c9..40f96b7 100644 --- a/retailcrm/config.xml +++ b/retailcrm/config.xml @@ -2,7 +2,7 @@ retailcrm - + diff --git a/retailcrm/config_ru.xml b/retailcrm/config_ru.xml index 754c073..46eb1b8 100644 --- a/retailcrm/config_ru.xml +++ b/retailcrm/config_ru.xml @@ -2,12 +2,12 @@ retailcrm - - + + 1 1 - + \ No newline at end of file diff --git a/retailcrm/export.tpl b/retailcrm/export.tpl deleted file mode 100644 index ead0361..0000000 --- a/retailcrm/export.tpl +++ /dev/null @@ -1,31 +0,0 @@ - - - - {$shop_name|escape} - {$company|escape} - {$shop_url|escape} - - {foreach from=$categories item=cat name=categories} - {if $cat.id_category > 2} - {$cat.name} - {/if} - {/foreach} - - - {foreach from=$products item=offer name=products} - - {if $offer.available_for_order}Y{else}N{/if} - {$offer.url|escape} - {$offer.price} - {$offer.purchase_price} - {$offer.id_category_default} - {$offer.picture|escape} - {$offer.name|escape} - {if $offer.article} - {$offer.article|escape} - {/if} - - {/foreach} - - - diff --git a/retailcrm/job/icml.php b/retailcrm/job/icml.php new file mode 100644 index 0000000..a5702d0 --- /dev/null +++ b/retailcrm/job/icml.php @@ -0,0 +1,21 @@ +exportCatalog(); + +$icml = new Icml( + Configuration::get('PS_SHOP_NAME'), + _PS_ROOT_DIR_ . '/retailcrm.xml' +); + +$icml->generate($data[0], $data[1]); diff --git a/retailcrm/job/sync.php b/retailcrm/job/sync.php new file mode 100644 index 0000000..5b73e6e --- /dev/null +++ b/retailcrm/job/sync.php @@ -0,0 +1,5 @@ +exportCatalog(); diff --git a/retailcrm/job_sync.php b/retailcrm/job_sync.php deleted file mode 100644 index 11e32a4..0000000 --- a/retailcrm/job_sync.php +++ /dev/null @@ -1,9 +0,0 @@ -orderHistory(); diff --git a/retailcrm/lib/classes/Catalog.php b/retailcrm/lib/classes/Catalog.php new file mode 100644 index 0000000..11c6aaa --- /dev/null +++ b/retailcrm/lib/classes/Catalog.php @@ -0,0 +1,98 @@ +default_lang = (int) Configuration::get('PS_LANG_DEFAULT'); + $this->default_currency = (int) Configuration::get('PS_CURRENCY_DEFAULT'); + $this->default_country = (int) Configuration::get('PS_COUNTRY_DEFAULT'); + } + + public function exportCatalog() + { + + $id_lang = (int) Configuration::get('PS_LANG_DEFAULT'); + $currency = new Currency(Configuration::get('PS_CURRENCY_DEFAULT')); + $shop_url = (Configuration::get('PS_SSL_ENABLED') ? _PS_BASE_URL_SSL_ : _PS_BASE_URL_); + + $items = array(); + $categories = array(); + + if ($currency->iso_code == 'RUB') { + $currency->iso_code = 'RUR'; + } + + // Get currencies + $currencies = Currency::getCurrencies(); + + // Get categories + $types = Category::getCategories($id_lang, true, false); + foreach ($types AS $category) + { + $categories[] = array( + 'id' => $category['id_category'], + 'parentId' => $category['id_parent'], + 'name' => $category['name'] + ); + } + + // Get products + $products = Product::getProducts($id_lang, 0, 0, 'name', 'asc'); + foreach ($products AS $product) + { + // Check for home category + $category = $product['id_category_default']; + + if ($category == Configuration::get('PS_HOME_CATEGORY')) { + $temp_categories = Product::getProductCategories($product['id_product']); + + foreach ($temp_categories AS $category) { + if ($category != Configuration::get('PS_HOME_CATEGORY')) + break; + } + + if ($category == Configuration::get('PS_HOME_CATEGORY')) { + continue; + } + + } + + $link = new Link(); + $cover = Image::getCover($product['id_product']); + + $picture = 'http://' . $link->getImageLink($product['link_rewrite'], $product['id_product'].'-'.$cover['id_image'], 'large_default'); + if (!(substr($picture, 0, strlen($shop_url)) === $shop_url)) { + $picture = rtrim($shop_url,"/") . $picture; + } + + $crewrite = Category::getLinkRewrite($product['id_category_default'], $id_lang); + $url = $link->getProductLink($product['id_product'], $product['link_rewrite'], $crewrite); + $version = substr(_PS_VERSION_, 0, 3); + + if ($version == "1.3") + $available_for_order = $product['active'] && $product['quantity']; + else { + $prod = new Product($product['id_product']); + $available_for_order = $product['active'] && $product['available_for_order'] && $prod->checkQty(1); + } + + $items[] = array( + 'id' => $product['id_product'], + 'productId' => $product['id_product'], + 'productActivity' => ($available_for_order) ? 'Y' : 'N', + 'initialPrice' => round($product['price'],2), + 'purchasePrice' => round($product['wholesale_price'], 2), + 'name' => htmlspecialchars(strip_tags($product['name'])), + 'productName' => htmlspecialchars(strip_tags($product['name'])), + 'categoryId' => array($category), + 'picture' => $picture, + 'url' => $url, + 'article' => htmlspecialchars($product['reference']) + ); + } + + return array($categories, $items); + + } +} diff --git a/retailcrm/lib/classes/OrderHistory.php b/retailcrm/lib/classes/OrderHistory.php new file mode 100644 index 0000000..50da3e7 --- /dev/null +++ b/retailcrm/lib/classes/OrderHistory.php @@ -0,0 +1,459 @@ +response = $this->api->ordersHistory( + $startDate = $startFrom, $endDate = $endTime, + $limit = 250, $offset = $counter + ); + $data = array_merge($data, $this->response); + $counter += 250; + } + catch (CurlException $e) { + error_log('orderHistory: connection error', 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); + } + catch (InvalidJsonException $e) { + error_log('orderHistory: ' . $e->getMessage(), 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); + } + } while (!empty($response)); + + /* + * store recieved data into shop database + */ + if (!empty($data)) { + $toUpdate = array(); + + /* + * Customer object. Will be used for further updates. + */ + $this->customer = new Customer(); + + $statuses = array_flip((array)json_decode(Configuration::get('RETAILCRM_API_STATUS'))); + $deliveries = array_flip((array)json_decode(Configuration::get('RETAILCRM_API_DELIVERY'))); + $payments = array_flip((array)json_decode(Configuration::get('RETAILCRM_API_PAYMENT'))); + + foreach ($data as $order) { + if (!array_key_exists('externalId', $order)) { + /* + * create customer if not exist + */ + $this->customer->getByEmail($order['customer']['email']); + + if (!array_key_exists('externalId', $order['customer'])) { + if (Validate::isEmail($order['customer']['email'])) { + + if (!$this->customer->id) + { + $this->customer->firstname = $order['customer']['firstName']; + $this->customer->lastname = $order['customer']['lastName']; + $this->customer->email = $order['customer']['email']; + $this->customer->passwd = substr(str_shuffle(strtolower(sha1(rand() . time()))),0, 5); + + if($this->customer->add()) { + + /* + * create customer address for delivery data + */ + + $this->customer->getByEmail($order['customer']['email']); + $this->customer_id = $this->customer->id; + + $address = new Address(); + $address->id_customer = $this->customer->id; + $address->id_country = $this->default_country; + $address->lastname = $this->customer->lastname; + $address->firstname = $this->customer->firstname; + $address->alias = 'default'; + $address->postcode = $order['deliveryAddress']['index']; + $address->city = $order['deliveryAddress']['city']; + $address->address1 = $order['deliveryAddress']['text']; + $address->phone = $order['phone']; + $address->phone_mobile = $order['phone']; + + $address->add(); + + /* + * store address record id for handle order data + */ + $addr = $this->customer->getAddresses($this->default_lang); + $this->address_id = $addr[0]['id_address']; + } + } else { + $addresses = $this->customer->getAddresses($this->default_lang); + $this->address_id = $addresses[0]['id_address']; + $this->customer_id = $this->customer->id; + } + + /* + * collect customer ids for single fix request + */ + array_push( + $this->customerFix, + array( + 'id' => $order['customer']['id'], + 'externalId' => $this->customer_id + ) + ); + } + } else { + $addresses = $this->customer->getAddresses($this->default_lang); + $this->address_id = $addresses[0]['id_address']; + $this->customer_id = $order['customer']['externalId']; + } + + $delivery = $order['deliveryType']; + $payment = $order['paymentType']; + $state = $order['status']; + + $cart = new Cart(); + $cart->id_currency = $this->default_currency; + $cart->id_lang = $this->default_lang; + $cart->id_customer = $this->customer_id; + $cart->id_address_delivery = (int) $this->address_id; + $cart->id_address_invoice = (int) $this->address_id; + $cart->id_carrier = (int) $deliveries[$delivery]; + + $cart->add(); + + $products = array(); + if(!empty($order['items'])) { + foreach ($order['items'] as $item) { + $product = array(); + $product['id_product'] = (int) $item['offer']['externalId']; + $product['quantity'] = $item['quantity']; + $product['id_address_delivery'] = (int) $this->address_id; + $products[] = $product; + } + } + + $cart->setWsCartRows($products); + $cart->update(); + + /* + * Create order + */ + + $newOrder = new Order(); + $newOrder->id_address_delivery = (int) $this->address_id; + $newOrder->id_address_invoice = (int) $this->address_id; + $newOrder->id_cart = (int) $cart->id; + $newOrder->id_currency = $this->default_currency; + $newOrder->id_lang = $this->default_lang; + $newOrder->id_customer = (int) $this->customer_id; + $newOrder->id_carrier = (int) $deliveries[$delivery]; + $newOrder->payment = $payments[$payment]; + $newOrder->module = (Module::getInstanceByName('advancedcheckout') === false) + ? $payments[$payment] + : 'advancedcheckout' + ; + $newOrder->total_paid = $order['summ'] + $order['deliveryCost']; + $newOrder->total_paid_tax_incl = $order['summ'] + $order['deliveryCost']; + $newOrder->total_paid_tax_excl = $order['summ'] + $order['deliveryCost']; + $newOrder->total_paid_real = $order['summ'] + $order['deliveryCost']; + $newOrder->total_products = $order['summ']; + $newOrder->total_products_wt = $order['summ']; + $newOrder->total_shipping = $order['deliveryCost']; + $newOrder->total_shipping_tax_incl = $order['deliveryCost']; + $newOrder->total_shipping_tax_excl = $order['deliveryCost']; + $newOrder->conversion_rate = 1.000000; + $newOrder->current_state = (int) $statuses[$state]; + $newOrder->delivery_date = $order['deliveryDate']; + $newOrder->date_add = $order['createdAt']; + $newOrder->date_upd = $order['createdAt']; + $newOrder->valid = 1; + $newOrder->secure_key = md5(time()); + + if (isset($order['discount'])) + { + $newOrder->total_discounts = $order['discount']; + } + + $newOrder->add(false, false); + + /* + * collect order ids for single fix request + */ + array_push( + $this->orderFix, + array( + 'id' => $order['id'], + 'externalId' => $newOrder->id + ) + ); + + /* + * Create order details + */ + $product_list = array(); + foreach ($order['items'] as $item) { + $product = new Product((int) $item['offer']['externalId'], false, $this->default_lang); + $qty = $item['quantity']; + $product_list[] = array('product' =>$product, 'quantity' => $qty); + } + + $query = 'INSERT `'._DB_PREFIX_.'order_detail` + ( + `id_order`, `id_order_invoice`, `id_shop`, `product_id`, `product_attribute_id`, + `product_name`, `product_quantity`, `product_quantity_in_stock`, `product_price`, + `product_reference`, `total_price_tax_excl`, `total_price_tax_incl`, + `unit_price_tax_excl`, `unit_price_tax_incl`, `original_product_price` + ) + + VALUES'; + + foreach ($product_list as $product) { + $query .= '(' + .(int) $newOrder->id.', + 0, + '. $this->context->shop->id.', + '.(int) $product['product']->id.', + 0, + '.implode('', array('\'', $product['product']->name, '\'')).', + '.(int) $product['quantity'].', + '.(int) $product['quantity'].', + '.$product['product']->price.', + '.implode('', array('\'', $product['product']->reference, '\'')).', + '.$product['product']->price.', + '.$product['product']->price.', + '.$product['product']->price.', + '.$product['product']->price.', + '.$product['product']->price.' + ),'; + } + + Db::getInstance()->execute(rtrim($query, ',')); + + try { + $this->api->customersFixExternalIds($this->customerFix); + $this->api->ordesrFixExternalIds($this->orderFix); + } + catch (CurlException $e) { + error_log('fixExternalId: connection error', 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); + continue; + } + catch (InvalidJsonException $e) { + error_log('fixExternalId: ' . $e->getMessage(), 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); + continue; + } + + } else { + if (!in_array($order['id'], $toUpdate)) + { + /* + * take last order update only + */ + $toUpdate[] = $order['id']; + if ($order['paymentType'] != null && $order['deliveryType'] != null && $order['status'] != null) { + $orderToUpdate = new Order((int) $order['externalId']); + + /* + * check status + */ + $stype = $order['status']; + if ($statuses[$stype] != null) { + if ($statuses[$stype] != $orderToUpdate->current_state) { + Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'orders` + SET `current_state` = \''.$statuses[$stype].'\' + WHERE `id_order` = '.(int) $order['externalId']); + } + } + + /* + * check delivery type + */ + $dtype = $order['deliveryType']; + if ($deliveries[$dtype] != null) { + if ($deliveries[$dtype] != $orderToUpdate->id_carrier) { + Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'orders` + SET `id_carrier` = \''.$deliveries[$dtype].'\' + WHERE `id_order` = '.(int) $order['externalId']); + Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'order_carrier` + SET `id_carrier` = \''.$deliveries[$dtype].'\' + WHERE `id_order` = \''.$orderToUpdate->id.'\''); + } + } + + /* + * check payment type + */ + $ptype = $order['paymentType']; + if ($payments[$ptype] != null) { + if ($payments[$ptype] != $orderToUpdate->payment) { + Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'orders` + SET `payment` = \''.$payments[$ptype].'\' + WHERE `id_order` = '.(int) $order['externalId']); + Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'order_payment` + SET `payment_method` = \''.$payments[$ptype].'\' + WHERE `order_reference` = \''.$orderToUpdate->reference.'\''); + + } + } + + /* + * check items + */ + + /* + * Clean deleted + */ + foreach ($order['items'] as $key => $item) { + if (isset($item['deleted']) && $item['deleted'] == true) { + Db::getInstance()->execute(' + DELETE FROM `'._DB_PREFIX_.'order_detail` + WHERE `id_order` = '. $orderToUpdate->id .' + AND `product_id` = '.$item['id']); + + unset($order['items'][$key]); + } + } + + /* + * check quantity + */ + + foreach ($orderToUpdate->getProductsDetail() as $orderItem) { + foreach ($order['items'] as $key => $item) { + if ($item['offer']['externalId'] == $orderItem['product_id']) { + if (isset($item['quantity']) && $item['quantity'] != $orderItem['product_quantity']) { + Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'order_detail` + SET `product_quantity` = '.$item['quantity'].', + `product_quantity_in_stock` = '.$item['quantity'].' + WHERE `id_order_detail` = '.$orderItem['id_order_detail']); + } + + unset($order['items'][$key]); + } + } + } + + /* + * check new items + */ + if (!empty($order['items'])) { + foreach ($order['items'] as $key => $newItem) { + $product = new Product((int) $newItem['offer']['externalId'], false, $this->default_lang); + $qty = $newItem['quantity']; + $product_list[] = array('product' =>$product, 'quantity' => $qty); + } + + + $query = 'INSERT `'._DB_PREFIX_.'order_detail` + ( + `id_order`, `id_order_invoice`, `id_shop`, `product_id`, `product_attribute_id`, + `product_name`, `product_quantity`, `product_quantity_in_stock`, `product_price`, + `product_reference`, `total_price_tax_excl`, `total_price_tax_incl`, + `unit_price_tax_excl`, `unit_price_tax_incl`, `original_product_price` + ) + + VALUES'; + + foreach ($product_list as $product) { + $query .= '(' + .(int) $orderToUpdate->id.', + 0, + '. $this->context->shop->id.', + '.(int) $product['product']->id.', + 0, + '.implode('', array('\'', $product['product']->name, '\'')).', + '.(int) $product['quantity'].', + '.(int) $product['quantity'].', + '.$product['product']->price.', + '.implode('', array('\'', $product['product']->reference, '\'')).', + '.$product['product']->price.', + '.$product['product']->price.', + '.$product['product']->price.', + '.$product['product']->price.', + '.$product['product']->price.' + ),'; + } + + Db::getInstance()->execute(rtrim($query, ',')); + unset($order['items'][$key]); + } + + /* + * Fix prices & discounts + * Discounts only for whole order + */ + $orderDiscout = null; + $orderTotal = $order['summ']; + + if (isset($order['discount']) && $order['discount'] > 0) { + if ($order['discount'] != $orderToUpdate->total_discounts) { + $orderDiscout = ($orderDiscout == null) ? $order['discount'] : $order['discount'] + $orderDiscout; + } + } + + if (isset($order['discountPercent']) && $order['discountPercent'] > 0) { + $percent = ($order['summ'] * $order['discountPercent'])/100; + if ($percent != $orderToUpdate->total_discounts) { + $orderDiscout = ($orderDiscout == null) ? $percent : $percent + $orderDiscout; + } + } + + $totalDiscount = ($orderDiscout == null) ? $orderToUpdate->total_discounts : $orderDiscout; + + if ($totalDiscount != $orderToUpdate->total_discounts || $orderTotal != $orderToUpdate->total_paid) { + Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'orders` + SET `total_discounts` = '.$totalDiscount.', + `total_discounts_tax_incl` = '.$totalDiscount.', + `total_discounts_tax_excl` = '.$totalDiscount.', + `total_paid` = '.$orderTotal.', + `total_paid_tax_incl` = '.$orderTotal.', + `total_paid_tax_excl` = '.$orderTotal.' + WHERE `id_order` = '.(int) $order['externalId']); + } + } + } + } + } + + /* + * Update last sync timestamp + */ + try { + Configuration::updateValue( + 'RETAILCRM_LAST_SYNC', + date_format($this->api->getGeneratedAt(), 'Y-m-d H:i:s') + ); + } + catch (CurlException $e) { + error_log('getLastSync: connection error', 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); + } + catch (InvalidJsonException $e) { + error_log('getLastSync: ' . $e->getMessage(), 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); + } + + return count($data) . " records was synced"; + + } else { + return 'Nothing to sync'; + } + + } +} \ No newline at end of file diff --git a/retailcrm/classes/References.php b/retailcrm/lib/classes/References.php similarity index 100% rename from retailcrm/classes/References.php rename to retailcrm/lib/classes/References.php diff --git a/retailcrm/lib/custom/.gitkeep b/retailcrm/lib/custom/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/retailcrm/classes/Icml.php b/retailcrm/lib/vendor/Icml.php similarity index 100% rename from retailcrm/classes/Icml.php rename to retailcrm/lib/vendor/Icml.php diff --git a/retailcrm/classes/Retailcrm.php b/retailcrm/lib/vendor/Retailcrm.php similarity index 100% rename from retailcrm/classes/Retailcrm.php rename to retailcrm/lib/vendor/Retailcrm.php diff --git a/retailcrm/classes/Service.php b/retailcrm/lib/vendor/Service.php similarity index 100% rename from retailcrm/classes/Service.php rename to retailcrm/lib/vendor/Service.php diff --git a/retailcrm/retailcrm.php b/retailcrm/retailcrm.php index cd6ee62..439bc89 100644 --- a/retailcrm/retailcrm.php +++ b/retailcrm/retailcrm.php @@ -1,26 +1,33 @@ name = 'retailcrm'; $this->tab = 'market_place'; - $this->version = '0.2'; + $this->version = '1.1'; $this->author = 'Retail Driver LCC'; $this->displayName = $this->l('RetailCRM'); @@ -119,7 +126,8 @@ class RetailCRM extends Module $output .= $this->displayConfirmation($this->l('Settings updated')); } } - $this->display(__FILE__, 'intarocrm.tpl'); + + $this->display(__FILE__, 'retailcrm.tpl'); return $output.$this->displayForm(); } @@ -282,676 +290,6 @@ class RetailCRM extends Module return $helper->generateForm($fields_form); } - public function hookNewOrder($params) - { - return $this->hookActionOrderStatusPostUpdate($params); - } - - public function hookActionPaymentConfirmation($params) - { - $this->api->ordersEdit( - array( - 'externalId' => $params['id_order'], - 'paymentStatus' => 'paid', - 'createdAt' => $params['cart']->date_upd - ) - ); - - return $this->hookActionOrderStatusPostUpdate($params); - } - - public function hookActionOrderStatusPostUpdate($params) - { - $address_id = Address::getFirstCustomerAddressId($params['cart']->id_customer); - $sql = 'SELECT * FROM '._DB_PREFIX_.'address WHERE id_address='.(int) $address_id; - $address = Db::getInstance()->ExecuteS($sql); - $address = $address[0]; - $delivery = json_decode(Configuration::get('RETAILCRM_API_DELIVERY')); - $payment = json_decode(Configuration::get('RETAILCRM_API_PAYMENT')); - $inCart = $params['cart']->getProducts(); - - if (isset($params['orderStatus'])) { - try { - $this->api->customersCreate( - array( - 'externalId' => $params['cart']->id_customer, - 'lastName' => $params['customer']->lastname, - 'firstName' => $params['customer']->firstname, - 'email' => $params['customer']->email, - 'phones' => array( - array( - 'number' => $address['phone'], - 'type' => 'mobile' - ), - array( - 'number' => $address['phone_mobile'], - 'type' => 'mobile' - ) - ), - 'createdAt' => $params['customer']->date_add - ) - ); - } - catch (CurlException $e) { - error_log("customerCreate: connection error", 3, _PS_ROOT_DIR . "log/retailcrm.log"); - } - catch (InvalidJsonException $e) { - error_log('customerCreate: ' . $e->getMessage(), 3, _PS_ROOT_DIR . "log/retailcrm.log"); - } - - try { - $items = array(); - foreach ($inCart as $item) { - $items[] = array( - 'initialPrice' => $item['price'], - 'quantity' => $item['quantity'], - 'productId' => $item['id_product'], - 'productName' => $item['name'], - 'createdAt' => $item['date_add'] - ); - } - - $dTypeKey = $params['cart']->id_carrier; - if (Module::getInstanceByName('advancedcheckout') === false) { - $pTypeKey = $params['order']->module; - } else { - $pTypeKey = $params['order']->payment; - } - $this->api->ordersCreate( - array( - 'externalId' => $params['order']->id, - 'orderType' => 'eshop-individual', - 'orderMethod' => 'shopping-cart', - 'customerId' => $params['cart']->id_customer, - 'firstName' => $params['customer']->firstname, - 'lastName' => $params['customer']->lastname, - 'phone' => $address['phone'], - 'email' => $params['customer']->email, - 'paymentStatus' => 'not-paid', - 'paymentType' => $payment->$pTypeKey, - 'deliveryType' => $delivery->$dTypeKey, - 'deliveryCost' => $params['order']->total_shipping, - 'status' => 'new', - 'deliveryAddress' => array( - 'city' => $address['city'], - 'index' => $address['postcode'], - 'text' => $address['address1'], - ), - 'discount' => $params['order']->total_discounts, - 'items' => $items, - 'createdAt' => $params['order']->date_add - ) - ); - } - catch (CurlException $e) { - error_log('orderCreate: connection error', 3, _PS_ROOT_DIR . "log/retailcrm.log"); - } - catch (InvalidJsonException $e) { - error_log('orderCreate: ' . $e->getMessage(), 3, _PS_ROOT_DIR . "log/retailcrm.log"); - } - - } - - if (isset($params['newOrderStatus']) && !empty($params['newOrderStatus'])) { - $statuses = OrderState::getOrderStates($this->default_lang); - $aStatuses = json_decode(Configuration::get('RETAILCRM_API_STATUS')); - foreach ($statuses as $status) { - if ($status['name'] == $params['newOrderStatus']->name) { - $currStatus = $status['id_order_state']; - try { - $this->api->ordersEdit( - array( - 'externalId' => $params['id_order'], - 'status' => $aStatuses->$currStatus, - 'createdAt' => $params['cart']->date_upd - ) - ); - } - catch (CurlException $e) { - error_log('orderStatusUpdate: connection error', 3, _PS_ROOT_DIR . "log/retailcrm.log"); - } - catch (InvalidJsonException $e) { - error_log('orderStatusUpdate: ' . $e->getMessage(), 3, _PS_ROOT_DIR . "log/retailcrm.log"); - } - } - } - } - } - - public function exportCatalog() - { - global $smarty; - $shop_url = (Configuration::get('PS_SSL_ENABLED') ? _PS_BASE_URL_SSL_ : _PS_BASE_URL_); - $id_lang = (int) Configuration::get('PS_LANG_DEFAULT'); - $currency = new Currency(Configuration::get('PS_CURRENCY_DEFAULT')); - if ($currency->iso_code == 'RUB') { - $currency->iso_code = 'RUR'; - } - - // Get currencies - $currencies = Currency::getCurrencies(); - - // Get categories - $categories = Category::getCategories($id_lang, true, false); - - // Get products - $products = Product::getProducts($id_lang, 0, 0, 'name', 'asc'); - foreach ($products AS $product) - { - // Check for home category - $category = $product['id_category_default']; - if ($category == Configuration::get('PS_HOME_CATEGORY')) { - $temp_categories = Product::getProductCategories($product['id_product']); - - foreach ($temp_categories AS $category) { - if ($category != Configuration::get('PS_HOME_CATEGORY')) - break; - } - - if ($category == Configuration::get('PS_HOME_CATEGORY')) { - continue; - } - - } - $link = new Link(); - $cover = Image::getCover($product['id_product']); - - $picture = 'http://' . $link->getImageLink($product['link_rewrite'], $product['id_product'].'-'.$cover['id_image'], 'large_default'); - if (!(substr($picture, 0, strlen($shop_url)) === $shop_url)) - $picture = rtrim($shop_url,"/") . $picture; - $crewrite = Category::getLinkRewrite($product['id_category_default'], $id_lang); - $url = $link->getProductLink($product['id_product'], $product['link_rewrite'], $crewrite); - $version = substr(_PS_VERSION_, 0, 3); - - if ($version == "1.3") - $available_for_order = $product['active'] && $product['quantity']; - else { - $prod = new Product($product['id_product']); - $available_for_order = $product['active'] && $product['available_for_order'] && $prod->checkQty(1); - } - - $items[] = array('id_product' => $product['id_product'], - 'available_for_order' => $available_for_order, - 'price' => round($product['price'],2), - 'purchase_price' => round($product['wholesale_price'], 2), - 'name' => htmlspecialchars(strip_tags($product['name'])), - 'article' => htmlspecialchars($product['reference']), - 'id_category_default' => $category, - 'picture' => $picture, - 'url' => $url - ); - } - - foreach ($this->custom_attributes as $i => $value) { - $attr = Configuration::get($value); - $smarty->assign(strtolower($value), $attr); - } - - $smarty->assign('currencies', $currencies); - $smarty->assign('currency', $currency->iso_code); - $smarty->assign('categories', $categories); - $smarty->assign('products', $items); - $smarty->assign('shop_name', Configuration::get('PS_SHOP_NAME')); - $smarty->assign('company', Configuration::get('PS_SHOP_NAME')); - $smarty->assign('shop_url', $shop_url . __PS_BASE_URI__); - return $this->display(__FILE__, 'export.tpl'); - } - - public function orderHistory() - { - /* - * get last sync date - */ - $lastSync = Configuration::get('RETAILCRM_LAST_SYNC'); - - $startFrom = ($lastSync === false) ? null : $lastSync; - $endTime = date('Y-m-d H:i:s'); - - $data = array(); - $counter = 0; - - /* - * retrive orders from crm since last update - */ - do { - try { - $this->response = $this->api->ordersHistory( - $startDate = $startFrom, $endDate = $endTime, - $limit = 250, $offset = $counter - ); - $data = array_merge($data, $this->response); - $counter += 250; - } - catch (CurlException $e) { - error_log('orderHistory: connection error', 3, _PS_ROOT_DIR . "log/retailcrm.log"); - } - catch (InvalidJsonException $e) { - error_log('orderHistory: ' . $e->getMessage(), 3, _PS_ROOT_DIR . "log/retailcrm.log"); - } - } while (!empty($response)); - - /* - * store recieved data into shop database - */ - if (!empty($data)) { - $toUpdate = array(); - - /* - * Customer object. Will be used for further updates. - */ - $this->customer = new Customer(); - - $statuses = array_flip((array)json_decode(Configuration::get('RETAILCRM_API_STATUS'))); - $deliveries = array_flip((array)json_decode(Configuration::get('RETAILCRM_API_DELIVERY'))); - $payments = array_flip((array)json_decode(Configuration::get('RETAILCRM_API_PAYMENT'))); - - foreach ($data as $order) { - if (!array_key_exists('externalId', $order)) { - /* - * create customer if not exist - */ - $this->customer->getByEmail($order['customer']['email']); - - if (!array_key_exists('externalId', $order['customer'])) { - if (Validate::isEmail($order['customer']['email'])) { - - if (!$this->customer->id) - { - $this->customer->firstname = $order['customer']['firstName']; - $this->customer->lastname = $order['customer']['lastName']; - $this->customer->email = $order['customer']['email']; - $this->customer->passwd = substr(str_shuffle(strtolower(sha1(rand() . time()))),0, 5); - - if($this->customer->add()) { - - /* - * create customer address for delivery data - */ - - $this->customer->getByEmail($order['customer']['email']); - $this->customer_id = $this->customer->id; - - $address = new Address(); - $address->id_customer = $this->customer->id; - $address->id_country = $this->default_country; - $address->lastname = $this->customer->lastname; - $address->firstname = $this->customer->firstname; - $address->alias = 'default'; - $address->postcode = $order['deliveryAddress']['index']; - $address->city = $order['deliveryAddress']['city']; - $address->address1 = $order['deliveryAddress']['text']; - $address->phone = $order['phone']; - $address->phone_mobile = $order['phone']; - - $address->add(); - - /* - * store address record id for handle order data - */ - $addr = $this->customer->getAddresses($this->default_lang); - $this->address_id = $addr[0]['id_address']; - } - } else { - $addresses = $this->customer->getAddresses($this->default_lang); - $this->address_id = $addresses[0]['id_address']; - $this->customer_id = $this->customer->id; - } - - /* - * collect customer ids for single fix request - */ - array_push( - $this->customerFix, - array( - 'id' => $order['customer']['id'], - 'externalId' => $this->customer_id - ) - ); - } - } else { - $addresses = $this->customer->getAddresses($this->default_lang); - $this->address_id = $addresses[0]['id_address']; - $this->customer_id = $order['customer']['externalId']; - } - - $delivery = $order['deliveryType']; - $payment = $order['paymentType']; - $state = $order['status']; - - $cart = new Cart(); - $cart->id_currency = $this->default_currency; - $cart->id_lang = $this->default_lang; - $cart->id_customer = $this->customer_id; - $cart->id_address_delivery = (int) $this->address_id; - $cart->id_address_invoice = (int) $this->address_id; - $cart->id_carrier = (int) $deliveries[$delivery]; - - $cart->add(); - - $products = array(); - if(!empty($order['items'])) { - foreach ($order['items'] as $item) { - $product = array(); - $product['id_product'] = (int) $item['offer']['externalId']; - $product['quantity'] = $item['quantity']; - $product['id_address_delivery'] = (int) $this->address_id; - $products[] = $product; - } - } - - $cart->setWsCartRows($products); - $cart->update(); - - /* - * Create order - */ - - $newOrder = new Order(); - $newOrder->id_address_delivery = (int) $this->address_id; - $newOrder->id_address_invoice = (int) $this->address_id; - $newOrder->id_cart = (int) $cart->id; - $newOrder->id_currency = $this->default_currency; - $newOrder->id_lang = $this->default_lang; - $newOrder->id_customer = (int) $this->customer_id; - $newOrder->id_carrier = (int) $deliveries[$delivery]; - $newOrder->payment = $payments[$payment]; - $newOrder->module = (Module::getInstanceByName('advancedcheckout') === false) - ? $payments[$payment] - : 'advancedcheckout' - ; - $newOrder->total_paid = $order['summ'] + $order['deliveryCost']; - $newOrder->total_paid_tax_incl = $order['summ'] + $order['deliveryCost']; - $newOrder->total_paid_tax_excl = $order['summ'] + $order['deliveryCost']; - $newOrder->total_paid_real = $order['summ'] + $order['deliveryCost']; - $newOrder->total_products = $order['summ']; - $newOrder->total_products_wt = $order['summ']; - $newOrder->total_shipping = $order['deliveryCost']; - $newOrder->total_shipping_tax_incl = $order['deliveryCost']; - $newOrder->total_shipping_tax_excl = $order['deliveryCost']; - $newOrder->conversion_rate = 1.000000; - $newOrder->current_state = (int) $statuses[$state]; - $newOrder->delivery_date = $order['deliveryDate']; - $newOrder->date_add = $order['createdAt']; - $newOrder->date_upd = $order['createdAt']; - $newOrder->valid = 1; - $newOrder->secure_key = md5(time()); - - if (isset($order['discount'])) - { - $newOrder->total_discounts = $order['discount']; - } - - $newOrder->add(false, false); - - /* - * collect order ids for single fix request - */ - array_push( - $this->orderFix, - array( - 'id' => $order['id'], - 'externalId' => $newOrder->id - ) - ); - - /* - * Create order details - */ - $product_list = array(); - foreach ($order['items'] as $item) { - $product = new Product((int) $item['offer']['externalId'], false, $this->default_lang); - $qty = $item['quantity']; - $product_list[] = array('product' =>$product, 'quantity' => $qty); - } - - $query = 'INSERT `'._DB_PREFIX_.'order_detail` - ( - `id_order`, `id_order_invoice`, `id_shop`, `product_id`, `product_attribute_id`, - `product_name`, `product_quantity`, `product_quantity_in_stock`, `product_price`, - `product_reference`, `total_price_tax_excl`, `total_price_tax_incl`, - `unit_price_tax_excl`, `unit_price_tax_incl`, `original_product_price` - ) - - VALUES'; - - foreach ($product_list as $product) { - $query .= '(' - .(int) $newOrder->id.', - 0, - '. $this->context->shop->id.', - '.(int) $product['product']->id.', - 0, - '.implode('', array('\'', $product['product']->name, '\'')).', - '.(int) $product['quantity'].', - '.(int) $product['quantity'].', - '.$product['product']->price.', - '.implode('', array('\'', $product['product']->reference, '\'')).', - '.$product['product']->price.', - '.$product['product']->price.', - '.$product['product']->price.', - '.$product['product']->price.', - '.$product['product']->price.' - ),'; - } - - Db::getInstance()->execute(rtrim($query, ',')); - - try { - $this->api->customersFixExternalIds($this->customerFix); - $this->api->ordesrFixExternalIds($this->orderFix); - } - catch (CurlException $e) { - error_log('fixExternalId: connection error', 3, _PS_ROOT_DIR . "log/retailcrm.log"); - continue; - } - catch (InvalidJsonException $e) { - error_log('fixExternalId: ' . $e->getMessage(), 3, _PS_ROOT_DIR . "log/retailcrm.log"); - continue; - } - - } else { - if (!in_array($order['id'], $toUpdate)) - { - /* - * take last order update only - */ - $toUpdate[] = $order['id']; - if ($order['paymentType'] != null && $order['deliveryType'] != null && $order['status'] != null) { - $orderToUpdate = new Order((int) $order['externalId']); - - /* - * check status - */ - $stype = $order['status']; - if ($statuses[$stype] != null) { - if ($statuses[$stype] != $orderToUpdate->current_state) { - Db::getInstance()->execute(' - UPDATE `'._DB_PREFIX_.'orders` - SET `current_state` = \''.$statuses[$stype].'\' - WHERE `id_order` = '.(int) $order['externalId']); - } - } - - /* - * check delivery type - */ - $dtype = $order['deliveryType']; - if ($deliveries[$dtype] != null) { - if ($deliveries[$dtype] != $orderToUpdate->id_carrier) { - Db::getInstance()->execute(' - UPDATE `'._DB_PREFIX_.'orders` - SET `id_carrier` = \''.$deliveries[$dtype].'\' - WHERE `id_order` = '.(int) $order['externalId']); - Db::getInstance()->execute(' - UPDATE `'._DB_PREFIX_.'order_carrier` - SET `id_carrier` = \''.$deliveries[$dtype].'\' - WHERE `id_order` = \''.$orderToUpdate->id.'\''); - } - } - - /* - * check payment type - */ - $ptype = $order['paymentType']; - if ($payments[$ptype] != null) { - if ($payments[$ptype] != $orderToUpdate->payment) { - Db::getInstance()->execute(' - UPDATE `'._DB_PREFIX_.'orders` - SET `payment` = \''.$payments[$ptype].'\' - WHERE `id_order` = '.(int) $order['externalId']); - Db::getInstance()->execute(' - UPDATE `'._DB_PREFIX_.'order_payment` - SET `payment_method` = \''.$payments[$ptype].'\' - WHERE `order_reference` = \''.$orderToUpdate->reference.'\''); - - } - } - - /* - * check items - */ - - /* - * Clean deleted - */ - foreach ($order['items'] as $key => $item) { - if (isset($item['deleted']) && $item['deleted'] == true) { - Db::getInstance()->execute(' - DELETE FROM `'._DB_PREFIX_.'order_detail` - WHERE `id_order` = '. $orderToUpdate->id .' - AND `product_id` = '.$item['id']); - - unset($order['items'][$key]); - } - } - - /* - * check quantity - */ - - foreach ($orderToUpdate->getProductsDetail() as $orderItem) { - foreach ($order['items'] as $key => $item) { - if ($item['offer']['externalId'] == $orderItem['product_id']) { - if (isset($item['quantity']) && $item['quantity'] != $orderItem['product_quantity']) { - Db::getInstance()->execute(' - UPDATE `'._DB_PREFIX_.'order_detail` - SET `product_quantity` = '.$item['quantity'].', - `product_quantity_in_stock` = '.$item['quantity'].' - WHERE `id_order_detail` = '.$orderItem['id_order_detail']); - } - - unset($order['items'][$key]); - } - } - } - - /* - * check new items - */ - if (!empty($order['items'])) { - foreach ($order['items'] as $key => $newItem) { - $product = new Product((int) $newItem['offer']['externalId'], false, $this->default_lang); - $qty = $newItem['quantity']; - $product_list[] = array('product' =>$product, 'quantity' => $qty); - } - - - $query = 'INSERT `'._DB_PREFIX_.'order_detail` - ( - `id_order`, `id_order_invoice`, `id_shop`, `product_id`, `product_attribute_id`, - `product_name`, `product_quantity`, `product_quantity_in_stock`, `product_price`, - `product_reference`, `total_price_tax_excl`, `total_price_tax_incl`, - `unit_price_tax_excl`, `unit_price_tax_incl`, `original_product_price` - ) - - VALUES'; - - foreach ($product_list as $product) { - $query .= '(' - .(int) $orderToUpdate->id.', - 0, - '. $this->context->shop->id.', - '.(int) $product['product']->id.', - 0, - '.implode('', array('\'', $product['product']->name, '\'')).', - '.(int) $product['quantity'].', - '.(int) $product['quantity'].', - '.$product['product']->price.', - '.implode('', array('\'', $product['product']->reference, '\'')).', - '.$product['product']->price.', - '.$product['product']->price.', - '.$product['product']->price.', - '.$product['product']->price.', - '.$product['product']->price.' - ),'; - } - - Db::getInstance()->execute(rtrim($query, ',')); - unset($order['items'][$key]); - } - - /* - * Fix prices & discounts - * Discounts only for whole order - */ - $orderDiscout = null; - $orderTotal = $order['summ']; - - if (isset($order['discount']) && $order['discount'] > 0) { - if ($order['discount'] != $orderToUpdate->total_discounts) { - $orderDiscout = ($orderDiscout == null) ? $order['discount'] : $order['discount'] + $orderDiscout; - } - } - - if (isset($order['discountPercent']) && $order['discountPercent'] > 0) { - $percent = ($order['summ'] * $order['discountPercent'])/100; - if ($percent != $orderToUpdate->total_discounts) { - $orderDiscout = ($orderDiscout == null) ? $percent : $percent + $orderDiscout; - } - } - - $totalDiscount = ($orderDiscout == null) ? $orderToUpdate->total_discounts : $orderDiscout; - - if ($totalDiscount != $orderToUpdate->total_discounts || $orderTotal != $orderToUpdate->total_paid) { - Db::getInstance()->execute(' - UPDATE `'._DB_PREFIX_.'orders` - SET `total_discounts` = '.$totalDiscount.', - `total_discounts_tax_incl` = '.$totalDiscount.', - `total_discounts_tax_excl` = '.$totalDiscount.', - `total_paid` = '.$orderTotal.', - `total_paid_tax_incl` = '.$orderTotal.', - `total_paid_tax_excl` = '.$orderTotal.' - WHERE `id_order` = '.(int) $order['externalId']); - } - } - } - } - } - - /* - * Update last sync timestamp - */ - try { - Configuration::updateValue( - 'RETAILCRM_LAST_SYNC', - date_format($this->api->getGeneratedAt(), 'Y-m-d H:i:s') - ); - } - catch (CurlException $e) { - error_log('getLastSync: connection error', 3, _PS_ROOT_DIR . "log/retailcrm.log"); - } - catch (InvalidJsonException $e) { - error_log('getLastSync: ' . $e->getMessage(), 3, _PS_ROOT_DIR . "log/retailcrm.log"); - } - - return count($data) . " records was synced"; - - } else { - return 'Nothing to sync'; - } - - } - public function getAddressFields() { $addressFields = array(); @@ -1048,4 +386,140 @@ class RetailCRM extends Module return $addressFields; } + + public function hookNewOrder($params) + { + return $this->hookActionOrderStatusPostUpdate($params); + } + + public function hookActionPaymentConfirmation($params) + { + $this->api->ordersEdit( + array( + 'externalId' => $params['id_order'], + 'paymentStatus' => 'paid', + 'createdAt' => $params['cart']->date_upd + ) + ); + + return $this->hookActionOrderStatusPostUpdate($params); + } + + public function hookActionOrderStatusPostUpdate($params) + { + $address_id = Address::getFirstCustomerAddressId($params['cart']->id_customer); + $sql = 'SELECT * FROM '._DB_PREFIX_.'address WHERE id_address='.(int) $address_id; + $address = Db::getInstance()->ExecuteS($sql); + $address = $address[0]; + $delivery = json_decode(Configuration::get('RETAILCRM_API_DELIVERY')); + $payment = json_decode(Configuration::get('RETAILCRM_API_PAYMENT')); + $inCart = $params['cart']->getProducts(); + + if (isset($params['orderStatus'])) { + try { + $this->api->customersCreate( + array( + 'externalId' => $params['cart']->id_customer, + 'lastName' => $params['customer']->lastname, + 'firstName' => $params['customer']->firstname, + 'email' => $params['customer']->email, + 'phones' => array( + array( + 'number' => $address['phone'], + 'type' => 'mobile' + ), + array( + 'number' => $address['phone_mobile'], + 'type' => 'mobile' + ) + ), + 'createdAt' => $params['customer']->date_add + ) + ); + } + catch (CurlException $e) { + error_log("customerCreate: connection error", 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); + } + catch (InvalidJsonException $e) { + error_log('customerCreate: ' . $e->getMessage(), 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); + } + + try { + $items = array(); + foreach ($inCart as $item) { + $items[] = array( + 'initialPrice' => $item['price'], + 'quantity' => $item['quantity'], + 'productId' => $item['id_product'], + 'productName' => $item['name'], + 'createdAt' => $item['date_add'] + ); + } + + $dTypeKey = $params['cart']->id_carrier; + if (Module::getInstanceByName('advancedcheckout') === false) { + $pTypeKey = $params['order']->module; + } else { + $pTypeKey = $params['order']->payment; + } + $this->api->ordersCreate( + array( + 'externalId' => $params['order']->id, + 'orderType' => 'eshop-individual', + 'orderMethod' => 'shopping-cart', + 'customerId' => $params['cart']->id_customer, + 'firstName' => $params['customer']->firstname, + 'lastName' => $params['customer']->lastname, + 'phone' => $address['phone'], + 'email' => $params['customer']->email, + 'paymentStatus' => 'not-paid', + 'paymentType' => $payment->$pTypeKey, + 'deliveryType' => $delivery->$dTypeKey, + 'deliveryCost' => $params['order']->total_shipping, + 'status' => 'new', + 'deliveryAddress' => array( + 'city' => $address['city'], + 'index' => $address['postcode'], + 'text' => $address['address1'], + ), + 'discount' => $params['order']->total_discounts, + 'items' => $items, + 'createdAt' => $params['order']->date_add + ) + ); + } + catch (CurlException $e) { + error_log('orderCreate: connection error', 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); + } + catch (InvalidJsonException $e) { + error_log('orderCreate: ' . $e->getMessage(), 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); + } + + } + + if (isset($params['newOrderStatus']) && !empty($params['newOrderStatus'])) { + $statuses = OrderState::getOrderStates($this->default_lang); + $aStatuses = json_decode(Configuration::get('RETAILCRM_API_STATUS')); + foreach ($statuses as $status) { + if ($status['name'] == $params['newOrderStatus']->name) { + $currStatus = $status['id_order_state']; + try { + $this->api->ordersEdit( + array( + 'externalId' => $params['id_order'], + 'status' => $aStatuses->$currStatus, + 'createdAt' => $params['cart']->date_upd + ) + ); + } + catch (CurlException $e) { + error_log('orderStatusUpdate: connection error', 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); + } + catch (InvalidJsonException $e) { + error_log('orderStatusUpdate: ' . $e->getMessage(), 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); + } + } + } + } + } } From 21c475b7b67ae88b4d1d8e3565efc1fcf9b278ec Mon Sep 17 00:00:00 2001 From: Alex Lushpai Date: Fri, 23 Oct 2015 17:50:13 +0300 Subject: [PATCH 07/17] update doc pics --- docs/images/add.png | Bin 15633 -> 11684 bytes docs/images/ref.png | Bin 53487 -> 39918 bytes docs/images/setup.png | Bin 59635 -> 11224 bytes 3 files changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/images/add.png b/docs/images/add.png index 68bfb2a147ec0da49cd689e171dc764669c7a4cf..c518ef58b8dd97c0d5d2f3fd7ab7627e793058fe 100644 GIT binary patch literal 11684 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 ys;sZ?ot&)J*cJR3pA(EnUB4tAQf&T-lttM3f literal 15633 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 UzHU3a3IV_S8V2g6s!u}y4{@in@Bjb+ literal 53487 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 z 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 diff --git a/docs/images/setup.png b/docs/images/setup.png index 18aa75ce17e785826228d94ec82368e315624e21..99c7fc91102fa8142e041fb3e3e5e8abcda7e5ca 100644 GIT binary patch literal 11224 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 jA`B%{L|Fb-htyrRctH}QOxLFb6g Date: Fri, 23 Oct 2015 17:52:20 +0300 Subject: [PATCH 08/17] License --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7942739 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 RetailDriver LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. From a25409571c1b90e4426f922ed311e67067de63bf Mon Sep 17 00:00:00 2001 From: Alex Lushpai Date: Mon, 26 Oct 2015 16:14:34 +0300 Subject: [PATCH 09/17] =?UTF-8?q?=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=20=D1=83=D1=87=D0=B5=D1=82=20tax=5Frate=20=D0=B4?= =?UTF-8?q?=D0=BB=D1=8F=20=D1=84=D0=BE=D1=80=D0=BC=D0=B8=D1=80=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D0=BD=D0=B8=D1=8F=20=D1=81=D1=82=D0=BE=D0=B8=D0=BC=D0=BE?= =?UTF-8?q?=D1=81=D1=82=D0=B8=20=D1=82=D0=BE=D0=B2=D0=B0=D1=80=D0=B0=20?= =?UTF-8?q?=D0=B2=20=D0=B7=D0=B0=D0=BA=D0=B0=D0=B7=D0=B5=20=D0=B8=20icml,?= =?UTF-8?q?=20=D0=B4=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B0=20?= =?UTF-8?q?=D0=B2=D0=BE=D0=B7=D0=BC=D0=BE=D0=B6=D0=BD=D0=BE=D1=81=D1=82?= =?UTF-8?q?=D1=8C=20=D0=B2=D1=8B=D0=B3=D1=80=D1=83=D0=B6=D0=B0=D1=82=D1=8C?= =?UTF-8?q?=20=D0=B2=20CRM=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D1=82=D0=B5=D0=BB=D1=8F=20=D0=BF=D0=BE=20=D1=84=D0=B0?= =?UTF-8?q?=D0=BA=D1=82=D1=83=20=D0=B5=D0=B3=D0=BE=20=D1=80=D0=B5=D0=B3?= =?UTF-8?q?=D0=B8=D1=81=D1=82=D1=80=D0=B0=D1=86=D0=B8=D0=B8=20=D0=B2=20CRM?= =?UTF-8?q?,=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B0=20=D1=81?= =?UTF-8?q?=D1=82=D1=80=D1=83=D0=BA=D1=82=D1=83=D1=80=D0=B0=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=B4=D0=B0=D0=B2=D0=B0=D0=B5=D0=BC=D1=8B=D1=85=20?= =?UTF-8?q?=D0=B2=20CRM=20=D0=B4=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D0=B7=D0=B0=D0=BA=D0=B0=D0=B7=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- retailcrm/job/icml.php | 2 +- retailcrm/lib/classes/Catalog.php | 51 +++++++++++++++++++++++++++---- retailcrm/lib/vendor/Icml.php | 1 + retailcrm/retailcrm.php | 46 +++++++++++++++++++++------- 4 files changed, 82 insertions(+), 18 deletions(-) diff --git a/retailcrm/job/icml.php b/retailcrm/job/icml.php index a5702d0..ec0ddec 100644 --- a/retailcrm/job/icml.php +++ b/retailcrm/job/icml.php @@ -11,7 +11,7 @@ if (file_exists(dirname(__FILE__) . '/../lib/custom/Catalog.php')) { } $job = new Catalog(); -$data = $job->exportCatalog(); +$data = $job->getData(); $icml = new Icml( Configuration::get('PS_SHOP_NAME'), diff --git a/retailcrm/lib/classes/Catalog.php b/retailcrm/lib/classes/Catalog.php index 11c6aaa..596ee8f 100644 --- a/retailcrm/lib/classes/Catalog.php +++ b/retailcrm/lib/classes/Catalog.php @@ -9,7 +9,7 @@ class Catalog $this->default_country = (int) Configuration::get('PS_COUNTRY_DEFAULT'); } - public function exportCatalog() + public function getData() { $id_lang = (int) Configuration::get('PS_LANG_DEFAULT'); @@ -39,6 +39,7 @@ class Catalog // Get products $products = Product::getProducts($id_lang, 0, 0, 'name', 'asc'); + foreach ($products AS $product) { // Check for home category @@ -77,19 +78,57 @@ class Catalog $available_for_order = $product['active'] && $product['available_for_order'] && $prod->checkQty(1); } - $items[] = array( + $item = array( 'id' => $product['id_product'], 'productId' => $product['id_product'], 'productActivity' => ($available_for_order) ? 'Y' : 'N', - 'initialPrice' => round($product['price'],2), - 'purchasePrice' => round($product['wholesale_price'], 2), 'name' => htmlspecialchars(strip_tags($product['name'])), 'productName' => htmlspecialchars(strip_tags($product['name'])), 'categoryId' => array($category), 'picture' => $picture, - 'url' => $url, - 'article' => htmlspecialchars($product['reference']) + 'url' => $url ); + + if (!empty($product['wholesale_price'])) { + $item['purchasePrice'] = round($product['wholesale_price'], 2); + } + + $item['initialPrice'] = !empty($product['rate']) + ? round($product['price'], 2) + (round($product['price'], 2) * $product['rate'] / 100) + : round($product['price'], 2) + ; + + + if (!empty($product['manufacturer_name'])) { + $item['vendor'] = $product['manufacturer_name']; + } + + if (!empty($product['reference'])) { + $item['article'] = htmlspecialchars($product['reference']); + } + + $weight = round($product['weight'], 2); + + if (!empty($weight)) { + $item['weight'] = $weight; + } + + $width = round($product['width'], 2); + $height = round($product['height'], 2); + $depth = round($product['depth'], 2); + + if (!empty($width)) { + if (!empty($height)) { + if (!empty($depth)) { + $item['size'] = implode('x', array($width, $height, $depth)); + } else { + $item['size'] = implode('x', array($width, $height)); + } + } + } + + $items[] = $item; + } return array($categories, $items); diff --git a/retailcrm/lib/vendor/Icml.php b/retailcrm/lib/vendor/Icml.php index 94009cd..2532c2b 100644 --- a/retailcrm/lib/vendor/Icml.php +++ b/retailcrm/lib/vendor/Icml.php @@ -32,6 +32,7 @@ class Icml 'color' => 'Цвет', 'weight' => 'Вес', 'size' => 'Размер', + 'tax' => 'Наценка' ); } diff --git a/retailcrm/retailcrm.php b/retailcrm/retailcrm.php index 439bc89..c43196f 100644 --- a/retailcrm/retailcrm.php +++ b/retailcrm/retailcrm.php @@ -68,8 +68,8 @@ class RetailCRM extends Module parent::install() && $this->registerHook('newOrder') && $this->registerHook('actionOrderStatusPostUpdate') && - $this->registerHook('actionPaymentConfirmation') - + $this->registerHook('actionPaymentConfirmation') && + $this->registerHook('actionCustomerAccountAdd') ); } @@ -387,6 +387,27 @@ class RetailCRM extends Module return $addressFields; } + public function hookActionCustomerAccountAdd($params) + { + try { + $this->api->customersCreate( + array( + 'externalId' => $params['newCustomer']->id, + 'firstName' => $params['newCustomer']->firstname, + 'lastName' => $params['newCustomer']->lastname, + 'email' => $params['newCustomer']->email, + 'createdAt' => $params['newCustomer']->date_add + ) + ); + } + catch (CurlException $e) { + error_log('customerCreate: connection error', 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); + } + catch (InvalidJsonException $e) { + error_log('customerCreate: ' . $e->getMessage(), 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); + } + } + public function hookNewOrder($params) { return $this->hookActionOrderStatusPostUpdate($params); @@ -448,7 +469,7 @@ class RetailCRM extends Module $items = array(); foreach ($inCart as $item) { $items[] = array( - 'initialPrice' => $item['price'], + 'initialPrice' => (!empty($item['rate'])) ? $item['price'] + ($item['price'] * $item['rate'] / 100) : $item['price'], 'quantity' => $item['quantity'], 'productId' => $item['id_product'], 'productName' => $item['name'], @@ -457,30 +478,33 @@ class RetailCRM extends Module } $dTypeKey = $params['cart']->id_carrier; + if (Module::getInstanceByName('advancedcheckout') === false) { $pTypeKey = $params['order']->module; } else { $pTypeKey = $params['order']->payment; } + $this->api->ordersCreate( array( 'externalId' => $params['order']->id, 'orderType' => 'eshop-individual', 'orderMethod' => 'shopping-cart', + 'status' => 'new', 'customerId' => $params['cart']->id_customer, 'firstName' => $params['customer']->firstname, 'lastName' => $params['customer']->lastname, 'phone' => $address['phone'], 'email' => $params['customer']->email, - 'paymentStatus' => 'not-paid', 'paymentType' => $payment->$pTypeKey, - 'deliveryType' => $delivery->$dTypeKey, - 'deliveryCost' => $params['order']->total_shipping, - 'status' => 'new', - 'deliveryAddress' => array( - 'city' => $address['city'], - 'index' => $address['postcode'], - 'text' => $address['address1'], + 'delivery' => array( + 'code' => $delivery->$dTypeKey, + 'cost' => $params['order']->total_shipping, + 'address' => array( + 'city' => $address['city'], + 'index' => $address['postcode'], + 'text' => $address['address1'], + ) ), 'discount' => $params['order']->total_discounts, 'items' => $items, From 92e5e30d6d473c15782ba98b82340931c9b1afa9 Mon Sep 17 00:00:00 2001 From: Alex Lushpai Date: Mon, 26 Oct 2015 17:53:20 +0300 Subject: [PATCH 10/17] orders/history --- retailcrm/job/sync.php | 436 +++++++++++++++++++++++ retailcrm/lib/classes/OrderHistory.php | 459 ------------------------- retailcrm/retailcrm.php | 5 +- 3 files changed, 438 insertions(+), 462 deletions(-) delete mode 100644 retailcrm/lib/classes/OrderHistory.php diff --git a/retailcrm/job/sync.php b/retailcrm/job/sync.php index 5b73e6e..b6ac5e0 100644 --- a/retailcrm/job/sync.php +++ b/retailcrm/job/sync.php @@ -1,5 +1,441 @@ ordersHistory(new DateTime($startFrom)); +} +catch (CurlException $e) { + error_log('orderHistory: connection error', 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); +} +catch (InvalidJsonException $e) { + error_log('orderHistory: ' . $e->getMessage(), 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); +} + +/* + * store recieved data into shop database +*/ +if (!empty($history->orders)) { + + /* + * Customer object. Will be used for further updates. + */ + $this->customer = new Customer(); + + $statuses = array_flip((array)json_decode(Configuration::get('RETAILCRM_API_STATUS'))); + $deliveries = array_flip((array)json_decode(Configuration::get('RETAILCRM_API_DELIVERY'))); + $payments = array_flip((array)json_decode(Configuration::get('RETAILCRM_API_PAYMENT'))); + + foreach ($history->orders as $order) { + if (!array_key_exists('externalId', $order)) { + /* + * create customer if not exist + */ + $this->customer->getByEmail($order['customer']['email']); + + if (!array_key_exists('externalId', $order['customer'])) { + if (Validate::isEmail($order['customer']['email'])) { + + if (!$this->customer->id) + { + $this->customer->firstname = $order['customer']['firstName']; + $this->customer->lastname = $order['customer']['lastName']; + $this->customer->email = $order['customer']['email']; + $this->customer->passwd = substr(str_shuffle(strtolower(sha1(rand() . time()))),0, 5); + + if($this->customer->add()) { + + /* + * create customer address for delivery data + */ + + $this->customer->getByEmail($order['customer']['email']); + $this->customer_id = $this->customer->id; + + $address = new Address(); + $address->id_customer = $this->customer->id; + $address->id_country = $this->default_country; + $address->lastname = $this->customer->lastname; + $address->firstname = $this->customer->firstname; + $address->alias = 'default'; + $address->postcode = $order['deliveryAddress']['index']; + $address->city = $order['deliveryAddress']['city']; + $address->address1 = $order['deliveryAddress']['text']; + $address->phone = $order['phone']; + $address->phone_mobile = $order['phone']; + + $address->add(); + + /* + * store address record id for handle order data + */ + $addr = $this->customer->getAddresses($this->default_lang); + $this->address_id = $addr[0]['id_address']; + } + } else { + $addresses = $this->customer->getAddresses($this->default_lang); + $this->address_id = $addresses[0]['id_address']; + $this->customer_id = $this->customer->id; + } + + /* + * collect customer ids for single fix request + */ + array_push( + $this->customerFix, + array( + 'id' => $order['customer']['id'], + 'externalId' => $this->customer_id + ) + ); + } + } else { + $addresses = $this->customer->getAddresses($this->default_lang); + $this->address_id = $addresses[0]['id_address']; + $this->customer_id = $order['customer']['externalId']; + } + + $delivery = $order['deliveryType']; + $payment = $order['paymentType']; + $state = $order['status']; + + $cart = new Cart(); + $cart->id_currency = $this->default_currency; + $cart->id_lang = $this->default_lang; + $cart->id_customer = $this->customer_id; + $cart->id_address_delivery = (int) $this->address_id; + $cart->id_address_invoice = (int) $this->address_id; + $cart->id_carrier = (int) $deliveries[$delivery]; + + $cart->add(); + + $products = array(); + if(!empty($order['items'])) { + foreach ($order['items'] as $item) { + $product = array(); + $product['id_product'] = (int) $item['offer']['externalId']; + $product['quantity'] = $item['quantity']; + $product['id_address_delivery'] = (int) $this->address_id; + $products[] = $product; + } + } + + $cart->setWsCartRows($products); + $cart->update(); + + /* + * Create order + */ + + $newOrder = new Order(); + $newOrder->id_address_delivery = (int) $this->address_id; + $newOrder->id_address_invoice = (int) $this->address_id; + $newOrder->id_cart = (int) $cart->id; + $newOrder->id_currency = $this->default_currency; + $newOrder->id_lang = $this->default_lang; + $newOrder->id_customer = (int) $this->customer_id; + $newOrder->id_carrier = (int) $deliveries[$delivery]; + $newOrder->payment = $payments[$payment]; + $newOrder->module = (Module::getInstanceByName('advancedcheckout') === false) + ? $payments[$payment] + : 'advancedcheckout' + ; + $newOrder->total_paid = $order['summ'] + $order['deliveryCost']; + $newOrder->total_paid_tax_incl = $order['summ'] + $order['deliveryCost']; + $newOrder->total_paid_tax_excl = $order['summ'] + $order['deliveryCost']; + $newOrder->total_paid_real = $order['summ'] + $order['deliveryCost']; + $newOrder->total_products = $order['summ']; + $newOrder->total_products_wt = $order['summ']; + $newOrder->total_shipping = $order['deliveryCost']; + $newOrder->total_shipping_tax_incl = $order['deliveryCost']; + $newOrder->total_shipping_tax_excl = $order['deliveryCost']; + $newOrder->conversion_rate = 1.000000; + $newOrder->current_state = (int) $statuses[$state]; + $newOrder->delivery_date = $order['deliveryDate']; + $newOrder->date_add = $order['createdAt']; + $newOrder->date_upd = $order['createdAt']; + $newOrder->valid = 1; + $newOrder->secure_key = md5(time()); + + if (isset($order['discount'])) + { + $newOrder->total_discounts = $order['discount']; + } + + $newOrder->add(false, false); + + /* + * collect order ids for single fix request + */ + array_push($this->orderFix, array('id' => $order['id'], 'externalId' => $newOrder->id)); + + /* + * Create order details + */ + $product_list = array(); + foreach ($order['items'] as $item) { + $product = new Product((int) $item['offer']['externalId'], false, $this->default_lang); + $qty = $item['quantity']; + $product_list[] = array('product' =>$product, 'quantity' => $qty); + } + + $query = 'INSERT `'._DB_PREFIX_.'order_detail` + ( + `id_order`, `id_order_invoice`, `id_shop`, `product_id`, `product_attribute_id`, + `product_name`, `product_quantity`, `product_quantity_in_stock`, `product_price`, + `product_reference`, `total_price_tax_excl`, `total_price_tax_incl`, + `unit_price_tax_excl`, `unit_price_tax_incl`, `original_product_price` + ) + + VALUES'; + + foreach ($product_list as $product) { + $query .= '(' + .(int) $newOrder->id.', + 0, + '. $this->context->shop->id.', + '.(int) $product['product']->id.', + 0, + '.implode('', array('\'', $product['product']->name, '\'')).', + '.(int) $product['quantity'].', + '.(int) $product['quantity'].', + '.$product['product']->price.', + '.implode('', array('\'', $product['product']->reference, '\'')).', + '.$product['product']->price.', + '.$product['product']->price.', + '.$product['product']->price.', + '.$product['product']->price.', + '.$product['product']->price.' + ),'; + } + + Db::getInstance()->execute(rtrim($query, ',')); + + try { + $this->api->customersFixExternalIds($this->customerFix); + $this->api->ordesrFixExternalIds($this->orderFix); + } + catch (CurlException $e) { + error_log('fixExternalId: connection error', 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); + continue; + } + catch (InvalidJsonException $e) { + error_log('fixExternalId: ' . $e->getMessage(), 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); + continue; + } + + } else { + if (!in_array($order['id'], $toUpdate)) + { + /* + * take last order update only + */ + + if ($order['paymentType'] != null && $order['deliveryType'] != null && $order['status'] != null) { + $orderToUpdate = new Order((int) $order['externalId']); + + /* + * check status + */ + $stype = $order['status']; + if ($statuses[$stype] != null) { + if ($statuses[$stype] != $orderToUpdate->current_state) { + Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'orders` + SET `current_state` = \''.$statuses[$stype].'\' + WHERE `id_order` = '.(int) $order['externalId']); + } + } + + /* + * check delivery type + */ + $dtype = $order['deliveryType']; + if ($deliveries[$dtype] != null) { + if ($deliveries[$dtype] != $orderToUpdate->id_carrier) { + Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'orders` + SET `id_carrier` = \''.$deliveries[$dtype].'\' + WHERE `id_order` = '.(int) $order['externalId']); + Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'order_carrier` + SET `id_carrier` = \''.$deliveries[$dtype].'\' + WHERE `id_order` = \''.$orderToUpdate->id.'\''); + } + } + + /* + * check payment type + */ + $ptype = $order['paymentType']; + if ($payments[$ptype] != null) { + if ($payments[$ptype] != $orderToUpdate->payment) { + Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'orders` + SET `payment` = \''.$payments[$ptype].'\' + WHERE `id_order` = '.(int) $order['externalId']); + Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'order_payment` + SET `payment_method` = \''.$payments[$ptype].'\' + WHERE `order_reference` = \''.$orderToUpdate->reference.'\''); + + } + } + + /* + * check items + */ + + /* + * Clean deleted + */ + foreach ($order['items'] as $key => $item) { + if (isset($item['deleted']) && $item['deleted'] == true) { + Db::getInstance()->execute(' + DELETE FROM `'._DB_PREFIX_.'order_detail` + WHERE `id_order` = '. $orderToUpdate->id .' + AND `product_id` = '.$item['id']); + + unset($order['items'][$key]); + } + } + + /* + * check quantity + */ + + foreach ($orderToUpdate->getProductsDetail() as $orderItem) { + foreach ($order['items'] as $key => $item) { + if ($item['offer']['externalId'] == $orderItem['product_id']) { + if (isset($item['quantity']) && $item['quantity'] != $orderItem['product_quantity']) { + Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'order_detail` + SET `product_quantity` = '.$item['quantity'].', + `product_quantity_in_stock` = '.$item['quantity'].' + WHERE `id_order_detail` = '.$orderItem['id_order_detail']); + } + + unset($order['items'][$key]); + } + } + } + + /* + * check new items + */ + if (!empty($order['items'])) { + foreach ($order['items'] as $key => $newItem) { + $product = new Product((int) $newItem['offer']['externalId'], false, $this->default_lang); + $qty = $newItem['quantity']; + $product_list[] = array('product' =>$product, 'quantity' => $qty); + } + $query = 'INSERT `'._DB_PREFIX_.'order_detail` + ( + `id_order`, `id_order_invoice`, `id_shop`, `product_id`, `product_attribute_id`, + `product_name`, `product_quantity`, `product_quantity_in_stock`, `product_price`, + `product_reference`, `total_price_tax_excl`, `total_price_tax_incl`, + `unit_price_tax_excl`, `unit_price_tax_incl`, `original_product_price` + ) + + VALUES'; + + foreach ($product_list as $product) { + $query .= '(' + .(int) $orderToUpdate->id.', + 0, + '. $this->context->shop->id.', + '.(int) $product['product']->id.', + 0, + '.implode('', array('\'', $product['product']->name, '\'')).', + '.(int) $product['quantity'].', + '.(int) $product['quantity'].', + '.$product['product']->price.', + '.implode('', array('\'', $product['product']->reference, '\'')).', + '.$product['product']->price.', + '.$product['product']->price.', + '.$product['product']->price.', + '.$product['product']->price.', + '.$product['product']->price.' + ),'; + } + + Db::getInstance()->execute(rtrim($query, ',')); + unset($order['items'][$key]); + } + + /* + * Fix prices & discounts + * Discounts only for whole order + */ + $orderDiscout = null; + $orderTotal = $order['summ']; + + if (isset($order['discount']) && $order['discount'] > 0) { + if ($order['discount'] != $orderToUpdate->total_discounts) { + $orderDiscout = ($orderDiscout == null) ? $order['discount'] : $order['discount'] + $orderDiscout; + } + } + + if (isset($order['discountPercent']) && $order['discountPercent'] > 0) { + $percent = ($order['summ'] * $order['discountPercent'])/100; + if ($percent != $orderToUpdate->total_discounts) { + $orderDiscout = ($orderDiscout == null) ? $percent : $percent + $orderDiscout; + } + } + + $totalDiscount = ($orderDiscout == null) ? $orderToUpdate->total_discounts : $orderDiscout; + + if ($totalDiscount != $orderToUpdate->total_discounts || $orderTotal != $orderToUpdate->total_paid) { + Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'orders` + SET `total_discounts` = '.$totalDiscount.', + `total_discounts_tax_incl` = '.$totalDiscount.', + `total_discounts_tax_excl` = '.$totalDiscount.', + `total_paid` = '.$orderTotal.', + `total_paid_tax_incl` = '.$orderTotal.', + `total_paid_tax_excl` = '.$orderTotal.' + WHERE `id_order` = '.(int) $order['externalId']); + } + } + } + } + } + + /* + * Update last sync timestamp + */ + Configuration::updateValue('RETAILCRM_LAST_SYNC', $history->generatedAt); +} else { + return 'Nothing to sync'; +} \ No newline at end of file diff --git a/retailcrm/lib/classes/OrderHistory.php b/retailcrm/lib/classes/OrderHistory.php deleted file mode 100644 index 50da3e7..0000000 --- a/retailcrm/lib/classes/OrderHistory.php +++ /dev/null @@ -1,459 +0,0 @@ -response = $this->api->ordersHistory( - $startDate = $startFrom, $endDate = $endTime, - $limit = 250, $offset = $counter - ); - $data = array_merge($data, $this->response); - $counter += 250; - } - catch (CurlException $e) { - error_log('orderHistory: connection error', 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); - } - catch (InvalidJsonException $e) { - error_log('orderHistory: ' . $e->getMessage(), 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); - } - } while (!empty($response)); - - /* - * store recieved data into shop database - */ - if (!empty($data)) { - $toUpdate = array(); - - /* - * Customer object. Will be used for further updates. - */ - $this->customer = new Customer(); - - $statuses = array_flip((array)json_decode(Configuration::get('RETAILCRM_API_STATUS'))); - $deliveries = array_flip((array)json_decode(Configuration::get('RETAILCRM_API_DELIVERY'))); - $payments = array_flip((array)json_decode(Configuration::get('RETAILCRM_API_PAYMENT'))); - - foreach ($data as $order) { - if (!array_key_exists('externalId', $order)) { - /* - * create customer if not exist - */ - $this->customer->getByEmail($order['customer']['email']); - - if (!array_key_exists('externalId', $order['customer'])) { - if (Validate::isEmail($order['customer']['email'])) { - - if (!$this->customer->id) - { - $this->customer->firstname = $order['customer']['firstName']; - $this->customer->lastname = $order['customer']['lastName']; - $this->customer->email = $order['customer']['email']; - $this->customer->passwd = substr(str_shuffle(strtolower(sha1(rand() . time()))),0, 5); - - if($this->customer->add()) { - - /* - * create customer address for delivery data - */ - - $this->customer->getByEmail($order['customer']['email']); - $this->customer_id = $this->customer->id; - - $address = new Address(); - $address->id_customer = $this->customer->id; - $address->id_country = $this->default_country; - $address->lastname = $this->customer->lastname; - $address->firstname = $this->customer->firstname; - $address->alias = 'default'; - $address->postcode = $order['deliveryAddress']['index']; - $address->city = $order['deliveryAddress']['city']; - $address->address1 = $order['deliveryAddress']['text']; - $address->phone = $order['phone']; - $address->phone_mobile = $order['phone']; - - $address->add(); - - /* - * store address record id for handle order data - */ - $addr = $this->customer->getAddresses($this->default_lang); - $this->address_id = $addr[0]['id_address']; - } - } else { - $addresses = $this->customer->getAddresses($this->default_lang); - $this->address_id = $addresses[0]['id_address']; - $this->customer_id = $this->customer->id; - } - - /* - * collect customer ids for single fix request - */ - array_push( - $this->customerFix, - array( - 'id' => $order['customer']['id'], - 'externalId' => $this->customer_id - ) - ); - } - } else { - $addresses = $this->customer->getAddresses($this->default_lang); - $this->address_id = $addresses[0]['id_address']; - $this->customer_id = $order['customer']['externalId']; - } - - $delivery = $order['deliveryType']; - $payment = $order['paymentType']; - $state = $order['status']; - - $cart = new Cart(); - $cart->id_currency = $this->default_currency; - $cart->id_lang = $this->default_lang; - $cart->id_customer = $this->customer_id; - $cart->id_address_delivery = (int) $this->address_id; - $cart->id_address_invoice = (int) $this->address_id; - $cart->id_carrier = (int) $deliveries[$delivery]; - - $cart->add(); - - $products = array(); - if(!empty($order['items'])) { - foreach ($order['items'] as $item) { - $product = array(); - $product['id_product'] = (int) $item['offer']['externalId']; - $product['quantity'] = $item['quantity']; - $product['id_address_delivery'] = (int) $this->address_id; - $products[] = $product; - } - } - - $cart->setWsCartRows($products); - $cart->update(); - - /* - * Create order - */ - - $newOrder = new Order(); - $newOrder->id_address_delivery = (int) $this->address_id; - $newOrder->id_address_invoice = (int) $this->address_id; - $newOrder->id_cart = (int) $cart->id; - $newOrder->id_currency = $this->default_currency; - $newOrder->id_lang = $this->default_lang; - $newOrder->id_customer = (int) $this->customer_id; - $newOrder->id_carrier = (int) $deliveries[$delivery]; - $newOrder->payment = $payments[$payment]; - $newOrder->module = (Module::getInstanceByName('advancedcheckout') === false) - ? $payments[$payment] - : 'advancedcheckout' - ; - $newOrder->total_paid = $order['summ'] + $order['deliveryCost']; - $newOrder->total_paid_tax_incl = $order['summ'] + $order['deliveryCost']; - $newOrder->total_paid_tax_excl = $order['summ'] + $order['deliveryCost']; - $newOrder->total_paid_real = $order['summ'] + $order['deliveryCost']; - $newOrder->total_products = $order['summ']; - $newOrder->total_products_wt = $order['summ']; - $newOrder->total_shipping = $order['deliveryCost']; - $newOrder->total_shipping_tax_incl = $order['deliveryCost']; - $newOrder->total_shipping_tax_excl = $order['deliveryCost']; - $newOrder->conversion_rate = 1.000000; - $newOrder->current_state = (int) $statuses[$state]; - $newOrder->delivery_date = $order['deliveryDate']; - $newOrder->date_add = $order['createdAt']; - $newOrder->date_upd = $order['createdAt']; - $newOrder->valid = 1; - $newOrder->secure_key = md5(time()); - - if (isset($order['discount'])) - { - $newOrder->total_discounts = $order['discount']; - } - - $newOrder->add(false, false); - - /* - * collect order ids for single fix request - */ - array_push( - $this->orderFix, - array( - 'id' => $order['id'], - 'externalId' => $newOrder->id - ) - ); - - /* - * Create order details - */ - $product_list = array(); - foreach ($order['items'] as $item) { - $product = new Product((int) $item['offer']['externalId'], false, $this->default_lang); - $qty = $item['quantity']; - $product_list[] = array('product' =>$product, 'quantity' => $qty); - } - - $query = 'INSERT `'._DB_PREFIX_.'order_detail` - ( - `id_order`, `id_order_invoice`, `id_shop`, `product_id`, `product_attribute_id`, - `product_name`, `product_quantity`, `product_quantity_in_stock`, `product_price`, - `product_reference`, `total_price_tax_excl`, `total_price_tax_incl`, - `unit_price_tax_excl`, `unit_price_tax_incl`, `original_product_price` - ) - - VALUES'; - - foreach ($product_list as $product) { - $query .= '(' - .(int) $newOrder->id.', - 0, - '. $this->context->shop->id.', - '.(int) $product['product']->id.', - 0, - '.implode('', array('\'', $product['product']->name, '\'')).', - '.(int) $product['quantity'].', - '.(int) $product['quantity'].', - '.$product['product']->price.', - '.implode('', array('\'', $product['product']->reference, '\'')).', - '.$product['product']->price.', - '.$product['product']->price.', - '.$product['product']->price.', - '.$product['product']->price.', - '.$product['product']->price.' - ),'; - } - - Db::getInstance()->execute(rtrim($query, ',')); - - try { - $this->api->customersFixExternalIds($this->customerFix); - $this->api->ordesrFixExternalIds($this->orderFix); - } - catch (CurlException $e) { - error_log('fixExternalId: connection error', 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); - continue; - } - catch (InvalidJsonException $e) { - error_log('fixExternalId: ' . $e->getMessage(), 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); - continue; - } - - } else { - if (!in_array($order['id'], $toUpdate)) - { - /* - * take last order update only - */ - $toUpdate[] = $order['id']; - if ($order['paymentType'] != null && $order['deliveryType'] != null && $order['status'] != null) { - $orderToUpdate = new Order((int) $order['externalId']); - - /* - * check status - */ - $stype = $order['status']; - if ($statuses[$stype] != null) { - if ($statuses[$stype] != $orderToUpdate->current_state) { - Db::getInstance()->execute(' - UPDATE `'._DB_PREFIX_.'orders` - SET `current_state` = \''.$statuses[$stype].'\' - WHERE `id_order` = '.(int) $order['externalId']); - } - } - - /* - * check delivery type - */ - $dtype = $order['deliveryType']; - if ($deliveries[$dtype] != null) { - if ($deliveries[$dtype] != $orderToUpdate->id_carrier) { - Db::getInstance()->execute(' - UPDATE `'._DB_PREFIX_.'orders` - SET `id_carrier` = \''.$deliveries[$dtype].'\' - WHERE `id_order` = '.(int) $order['externalId']); - Db::getInstance()->execute(' - UPDATE `'._DB_PREFIX_.'order_carrier` - SET `id_carrier` = \''.$deliveries[$dtype].'\' - WHERE `id_order` = \''.$orderToUpdate->id.'\''); - } - } - - /* - * check payment type - */ - $ptype = $order['paymentType']; - if ($payments[$ptype] != null) { - if ($payments[$ptype] != $orderToUpdate->payment) { - Db::getInstance()->execute(' - UPDATE `'._DB_PREFIX_.'orders` - SET `payment` = \''.$payments[$ptype].'\' - WHERE `id_order` = '.(int) $order['externalId']); - Db::getInstance()->execute(' - UPDATE `'._DB_PREFIX_.'order_payment` - SET `payment_method` = \''.$payments[$ptype].'\' - WHERE `order_reference` = \''.$orderToUpdate->reference.'\''); - - } - } - - /* - * check items - */ - - /* - * Clean deleted - */ - foreach ($order['items'] as $key => $item) { - if (isset($item['deleted']) && $item['deleted'] == true) { - Db::getInstance()->execute(' - DELETE FROM `'._DB_PREFIX_.'order_detail` - WHERE `id_order` = '. $orderToUpdate->id .' - AND `product_id` = '.$item['id']); - - unset($order['items'][$key]); - } - } - - /* - * check quantity - */ - - foreach ($orderToUpdate->getProductsDetail() as $orderItem) { - foreach ($order['items'] as $key => $item) { - if ($item['offer']['externalId'] == $orderItem['product_id']) { - if (isset($item['quantity']) && $item['quantity'] != $orderItem['product_quantity']) { - Db::getInstance()->execute(' - UPDATE `'._DB_PREFIX_.'order_detail` - SET `product_quantity` = '.$item['quantity'].', - `product_quantity_in_stock` = '.$item['quantity'].' - WHERE `id_order_detail` = '.$orderItem['id_order_detail']); - } - - unset($order['items'][$key]); - } - } - } - - /* - * check new items - */ - if (!empty($order['items'])) { - foreach ($order['items'] as $key => $newItem) { - $product = new Product((int) $newItem['offer']['externalId'], false, $this->default_lang); - $qty = $newItem['quantity']; - $product_list[] = array('product' =>$product, 'quantity' => $qty); - } - - - $query = 'INSERT `'._DB_PREFIX_.'order_detail` - ( - `id_order`, `id_order_invoice`, `id_shop`, `product_id`, `product_attribute_id`, - `product_name`, `product_quantity`, `product_quantity_in_stock`, `product_price`, - `product_reference`, `total_price_tax_excl`, `total_price_tax_incl`, - `unit_price_tax_excl`, `unit_price_tax_incl`, `original_product_price` - ) - - VALUES'; - - foreach ($product_list as $product) { - $query .= '(' - .(int) $orderToUpdate->id.', - 0, - '. $this->context->shop->id.', - '.(int) $product['product']->id.', - 0, - '.implode('', array('\'', $product['product']->name, '\'')).', - '.(int) $product['quantity'].', - '.(int) $product['quantity'].', - '.$product['product']->price.', - '.implode('', array('\'', $product['product']->reference, '\'')).', - '.$product['product']->price.', - '.$product['product']->price.', - '.$product['product']->price.', - '.$product['product']->price.', - '.$product['product']->price.' - ),'; - } - - Db::getInstance()->execute(rtrim($query, ',')); - unset($order['items'][$key]); - } - - /* - * Fix prices & discounts - * Discounts only for whole order - */ - $orderDiscout = null; - $orderTotal = $order['summ']; - - if (isset($order['discount']) && $order['discount'] > 0) { - if ($order['discount'] != $orderToUpdate->total_discounts) { - $orderDiscout = ($orderDiscout == null) ? $order['discount'] : $order['discount'] + $orderDiscout; - } - } - - if (isset($order['discountPercent']) && $order['discountPercent'] > 0) { - $percent = ($order['summ'] * $order['discountPercent'])/100; - if ($percent != $orderToUpdate->total_discounts) { - $orderDiscout = ($orderDiscout == null) ? $percent : $percent + $orderDiscout; - } - } - - $totalDiscount = ($orderDiscout == null) ? $orderToUpdate->total_discounts : $orderDiscout; - - if ($totalDiscount != $orderToUpdate->total_discounts || $orderTotal != $orderToUpdate->total_paid) { - Db::getInstance()->execute(' - UPDATE `'._DB_PREFIX_.'orders` - SET `total_discounts` = '.$totalDiscount.', - `total_discounts_tax_incl` = '.$totalDiscount.', - `total_discounts_tax_excl` = '.$totalDiscount.', - `total_paid` = '.$orderTotal.', - `total_paid_tax_incl` = '.$orderTotal.', - `total_paid_tax_excl` = '.$orderTotal.' - WHERE `id_order` = '.(int) $order['externalId']); - } - } - } - } - } - - /* - * Update last sync timestamp - */ - try { - Configuration::updateValue( - 'RETAILCRM_LAST_SYNC', - date_format($this->api->getGeneratedAt(), 'Y-m-d H:i:s') - ); - } - catch (CurlException $e) { - error_log('getLastSync: connection error', 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); - } - catch (InvalidJsonException $e) { - error_log('getLastSync: ' . $e->getMessage(), 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); - } - - return count($data) . " records was synced"; - - } else { - return 'Nothing to sync'; - } - - } -} \ No newline at end of file diff --git a/retailcrm/retailcrm.php b/retailcrm/retailcrm.php index c43196f..90b258b 100644 --- a/retailcrm/retailcrm.php +++ b/retailcrm/retailcrm.php @@ -521,7 +521,7 @@ class RetailCRM extends Module } - if (isset($params['newOrderStatus']) && !empty($params['newOrderStatus'])) { + if (!empty($params['newOrderStatus'])) { $statuses = OrderState::getOrderStates($this->default_lang); $aStatuses = json_decode(Configuration::get('RETAILCRM_API_STATUS')); foreach ($statuses as $status) { @@ -531,8 +531,7 @@ class RetailCRM extends Module $this->api->ordersEdit( array( 'externalId' => $params['id_order'], - 'status' => $aStatuses->$currStatus, - 'createdAt' => $params['cart']->date_upd + 'status' => $aStatuses->$currStatus ) ); } From 88fcb88deeb48d8293cebdcab117cd697863b184 Mon Sep 17 00:00:00 2001 From: Alex Lushpai Date: Mon, 26 Oct 2015 18:09:02 +0300 Subject: [PATCH 11/17] changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e98883b..5cc9d14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,12 @@ Changelog ========= +v1.1 + +* Выгрузка клиентов при регистрации +* Учет наценок (НДС) при передаче данных по заказу в CRM +* Учет наценок (НДС) при генерации ICML + v0.2 * Ребрендинг, intarocrm заменено на retailcrm From 52ee86eb71bba3e217786cf9521b61d4270524a3 Mon Sep 17 00:00:00 2001 From: Alex Lushpai Date: Tue, 27 Oct 2015 17:31:53 +0300 Subject: [PATCH 12/17] =?UTF-8?q?=D0=BF=D0=B5=D1=80=D0=B5=D1=80=D0=B0?= =?UTF-8?q?=D0=B1=D0=BE=D1=82=D0=BA=D0=B0=20=D1=81=D0=BA=D1=80=D0=B8=D0=BF?= =?UTF-8?q?=D1=82=D0=B0=20=D0=BE=D0=B1=D1=80=D0=B0=D1=82=D0=BD=D0=BE=D0=B9?= =?UTF-8?q?=20=D0=B8=D0=BD=D1=82=D0=B5=D0=B3=D1=80=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- retailcrm/job/sync.php | 579 ++++++++++++++++++++++------------------ retailcrm/retailcrm.php | 166 ++++++------ 2 files changed, 396 insertions(+), 349 deletions(-) diff --git a/retailcrm/job/sync.php b/retailcrm/job/sync.php index b6ac5e0..d567a50 100644 --- a/retailcrm/job/sync.php +++ b/retailcrm/job/sync.php @@ -9,6 +9,10 @@ if (file_exists(dirname(__FILE__) . '/../lib/custom/References.php')) { require(dirname(__FILE__) . '/../lib/classes/References.php'); } +$default_lang = (int) Configuration::get('PS_LANG_DEFAULT'); +$default_currency = (int) Configuration::get('PS_CURRENCY_DEFAULT'); +$default_country = (int) Configuration::get('PS_COUNTRY_DEFAULT'); + $apiUrl = Configuration::get('RETAILCRM_ADDRESS'); $apiKey = Configuration::get('RETAILCRM_API_TOKEN'); @@ -27,6 +31,10 @@ $startFrom = ($lastSync === false) ; $history = array(); +$customerFix = array(); +$orderFix = array(); + +$address_id = 0; /* * retrive orders from crm since last update @@ -44,107 +52,129 @@ catch (InvalidJsonException $e) { /* * store recieved data into shop database */ -if (!empty($history->orders)) { +if (count($history->orders) > 0) { /* * Customer object. Will be used for further updates. */ - $this->customer = new Customer(); - $statuses = array_flip((array)json_decode(Configuration::get('RETAILCRM_API_STATUS'))); - $deliveries = array_flip((array)json_decode(Configuration::get('RETAILCRM_API_DELIVERY'))); - $payments = array_flip((array)json_decode(Configuration::get('RETAILCRM_API_PAYMENT'))); + $statuses = array_filter( + json_decode( + Configuration::get('RETAILCRM_API_STATUS'), true + ) + ); + + $statuses = array_flip($statuses); + + $deliveries = array_filter( + json_decode( + Configuration::get('RETAILCRM_API_DELIVERY'), true + ) + ); + + $deliveries = array_flip($deliveries); + + $payments = array_filter( + json_decode( + Configuration::get('RETAILCRM_API_PAYMENT'), true + ) + ); + + $payments = array_flip($payments); foreach ($history->orders as $order) { + if (!array_key_exists('externalId', $order)) { /* * create customer if not exist */ - $this->customer->getByEmail($order['customer']['email']); + + $customer = new Customer(); + $customer->getByEmail($order['customer']['email']); if (!array_key_exists('externalId', $order['customer'])) { if (Validate::isEmail($order['customer']['email'])) { - if (!$this->customer->id) + if (!$customer->id) { - $this->customer->firstname = $order['customer']['firstName']; - $this->customer->lastname = $order['customer']['lastName']; - $this->customer->email = $order['customer']['email']; - $this->customer->passwd = substr(str_shuffle(strtolower(sha1(rand() . time()))),0, 5); + $customer->firstname = $order['customer']['firstName']; + $customer->lastname = $order['customer']['lastName']; + $customer->email = $order['customer']['email']; + $customer->passwd = substr(str_shuffle(strtolower(sha1(rand() . time()))),0, 5); - if($this->customer->add()) { + if($customer->add()) { /* * create customer address for delivery data */ - $this->customer->getByEmail($order['customer']['email']); - $this->customer_id = $this->customer->id; + $customer->getByEmail($order['customer']['email']); + $customer_id = $customer->id; $address = new Address(); - $address->id_customer = $this->customer->id; - $address->id_country = $this->default_country; - $address->lastname = $this->customer->lastname; - $address->firstname = $this->customer->firstname; + $address->id_customer = $customer->id; + $address->id_country = $default_country; + $address->lastname = $customer->lastname; + $address->firstname = $customer->firstname; $address->alias = 'default'; - $address->postcode = $order['deliveryAddress']['index']; - $address->city = $order['deliveryAddress']['city']; - $address->address1 = $order['deliveryAddress']['text']; - $address->phone = $order['phone']; - $address->phone_mobile = $order['phone']; + $address->postcode = $customer['address']['index']; + $address->city = $customer['address']['city']; + $address->address1 = $customer['address']['text']; + $address->phone = $customer['phones'][0]['number']; $address->add(); /* * store address record id for handle order data */ - $addr = $this->customer->getAddresses($this->default_lang); - $this->address_id = $addr[0]['id_address']; + $addr = $customer->getAddresses($default_lang); + $address_id = $addr[0]['id_address']; } } else { - $addresses = $this->customer->getAddresses($this->default_lang); - $this->address_id = $addresses[0]['id_address']; - $this->customer_id = $this->customer->id; + $addresses = $customer->getAddresses($default_lang); + $address_id = $addresses[0]['id_address']; + $customer_id = $customer->id; } /* * collect customer ids for single fix request */ array_push( - $this->customerFix, - array( - 'id' => $order['customer']['id'], - 'externalId' => $this->customer_id - ) + $customerFix, + array( + 'id' => $order['customer']['id'], + 'externalId' => $customer_id + ) ); } } else { - $addresses = $this->customer->getAddresses($this->default_lang); - $this->address_id = $addresses[0]['id_address']; - $this->customer_id = $order['customer']['externalId']; + $addresses = $customer->getAddresses($default_lang); + $address_id = $addresses[0]['id_address']; + $customer_id = $order['customer']['externalId']; } - $delivery = $order['deliveryType']; + $delivery = $order['delivery']['code']; $payment = $order['paymentType']; $state = $order['status']; $cart = new Cart(); - $cart->id_currency = $this->default_currency; - $cart->id_lang = $this->default_lang; - $cart->id_customer = $this->customer_id; - $cart->id_address_delivery = (int) $this->address_id; - $cart->id_address_invoice = (int) $this->address_id; + $cart->id_currency = $default_currency; + $cart->id_lang = $default_lang; + $cart->id_customer = $customer_id; + $cart->id_address_delivery = (int) $address_id; + $cart->id_address_invoice = (int) $address_id; $cart->id_carrier = (int) $deliveries[$delivery]; $cart->add(); $products = array(); + if(!empty($order['items'])) { foreach ($order['items'] as $item) { $product = array(); $product['id_product'] = (int) $item['offer']['externalId']; $product['quantity'] = $item['quantity']; - $product['id_address_delivery'] = (int) $this->address_id; + $product['id_address_delivery'] = (int) $address_id; $products[] = $product; } } @@ -157,58 +187,225 @@ if (!empty($history->orders)) { */ $newOrder = new Order(); - $newOrder->id_address_delivery = (int) $this->address_id; - $newOrder->id_address_invoice = (int) $this->address_id; + $newOrder->id_address_delivery = (int) $address_id; + $newOrder->id_address_invoice = (int) $address_id; $newOrder->id_cart = (int) $cart->id; - $newOrder->id_currency = $this->default_currency; - $newOrder->id_lang = $this->default_lang; - $newOrder->id_customer = (int) $this->customer_id; + $newOrder->id_currency = $default_currency; + $newOrder->id_lang = $default_lang; + $newOrder->id_customer = (int) $customer_id; $newOrder->id_carrier = (int) $deliveries[$delivery]; $newOrder->payment = $payments[$payment]; $newOrder->module = (Module::getInstanceByName('advancedcheckout') === false) - ? $payments[$payment] - : 'advancedcheckout' + ? $payments[$payment] + : 'advancedcheckout' ; - $newOrder->total_paid = $order['summ'] + $order['deliveryCost']; - $newOrder->total_paid_tax_incl = $order['summ'] + $order['deliveryCost']; - $newOrder->total_paid_tax_excl = $order['summ'] + $order['deliveryCost']; - $newOrder->total_paid_real = $order['summ'] + $order['deliveryCost']; - $newOrder->total_products = $order['summ']; - $newOrder->total_products_wt = $order['summ']; - $newOrder->total_shipping = $order['deliveryCost']; - $newOrder->total_shipping_tax_incl = $order['deliveryCost']; - $newOrder->total_shipping_tax_excl = $order['deliveryCost']; - $newOrder->conversion_rate = 1.000000; - $newOrder->current_state = (int) $statuses[$state]; - $newOrder->delivery_date = $order['deliveryDate']; - $newOrder->date_add = $order['createdAt']; - $newOrder->date_upd = $order['createdAt']; - $newOrder->valid = 1; - $newOrder->secure_key = md5(time()); + $newOrder->total_paid = $order['summ'] + $order['delivery']['cost']; + $newOrder->total_paid_tax_incl = $order['summ'] + $order['delivery']['cost']; + $newOrder->total_paid_tax_excl = $order['summ'] + $order['delivery']['cost']; + $newOrder->total_paid_real = $order['summ'] + $order['delivery']['cost']; + $newOrder->total_products = $order['summ']; + $newOrder->total_products_wt = $order['summ']; + $newOrder->total_shipping = $order['delivery']['cost']; + $newOrder->total_shipping_tax_incl = $order['delivery']['cost']; + $newOrder->total_shipping_tax_excl = $order['delivery']['cost']; + $newOrder->conversion_rate = 1.000000; + $newOrder->current_state = (int) $statuses[$state]; + $newOrder->delivery_date = $order['delivery']['date']; + $newOrder->date_add = $order['createdAt']; + $newOrder->date_upd = $order['createdAt']; + $newOrder->valid = 1; + $newOrder->secure_key = md5(time()); - if (isset($order['discount'])) - { - $newOrder->total_discounts = $order['discount']; - } + if (isset($order['discount'])) + { + $newOrder->total_discounts = $order['discount']; + } - $newOrder->add(false, false); + $newOrder->add(false, false); + + /* + * collect order ids for single fix request + */ + array_push($orderFix, array('id' => $order['id'], 'externalId' => $newOrder->id)); + + /* + * Create order details + */ + $product_list = array(); + foreach ($order['items'] as $item) { + $product = new Product((int) $item['offer']['externalId'], false, $default_lang); + $qty = $item['quantity']; + $product_list[] = array('product' =>$product, 'quantity' => $qty); + } + + $query = 'INSERT `'._DB_PREFIX_.'order_detail` + ( + `id_order`, `id_order_invoice`, `id_shop`, `product_id`, `product_attribute_id`, + `product_name`, `product_quantity`, `product_quantity_in_stock`, `product_price`, + `product_reference`, `total_price_tax_excl`, `total_price_tax_incl`, + `unit_price_tax_excl`, `unit_price_tax_incl`, `original_product_price` + ) + + VALUES'; + + foreach ($product_list as $product) { + $query .= '(' + .(int) $newOrder->id.', + 0, + '. $this->context->shop->id.', + '.(int) $product['product']->id.', + 0, + '.implode('', array('\'', $product['product']->name, '\'')).', + '.(int) $product['quantity'].', + '.(int) $product['quantity'].', + '.$product['product']->price.', + '.implode('', array('\'', $product['product']->reference, '\'')).', + '.$product['product']->price.', + '.$product['product']->price.', + '.$product['product']->price.', + '.$product['product']->price.', + '.$product['product']->price.' + ),'; + } + + Db::getInstance()->execute(rtrim($query, ',')); + + try { + $this->api->customersFixExternalIds($customerFix); + } + catch (CurlException $e) { + error_log('customersFixExternalId: connection error', 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); + continue; + } + catch (InvalidJsonException $e) { + error_log('customersFixExternalId: ' . $e->getMessage(), 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); + continue; + } + + try { + $this->api->ordersFixExternalIds($orderFix); + } + catch (CurlException $e) { + error_log('ordersFixExternalId: connection error', 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); + continue; + } + catch (InvalidJsonException $e) { + error_log('ordersFixExternalId: ' . $e->getMessage(), 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); + continue; + } + } else { + /* + * take last order update only + */ + + if ($order['paymentType'] != null && $order['deliveryType'] != null && $order['status'] != null) { + $orderToUpdate = new Order((int) $order['externalId']); /* - * collect order ids for single fix request + * check status */ - array_push($this->orderFix, array('id' => $order['id'], 'externalId' => $newOrder->id)); - - /* - * Create order details - */ - $product_list = array(); - foreach ($order['items'] as $item) { - $product = new Product((int) $item['offer']['externalId'], false, $this->default_lang); - $qty = $item['quantity']; - $product_list[] = array('product' =>$product, 'quantity' => $qty); + $stype = $order['status']; + if ($statuses[$stype] != null) { + if ($statuses[$stype] != $orderToUpdate->current_state) { + Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'orders` + SET `current_state` = \''.$statuses[$stype].'\' + WHERE `id_order` = '.(int) $order['externalId'] + ); + } } - $query = 'INSERT `'._DB_PREFIX_.'order_detail` + /* + * check delivery type + */ + $dtype = $order['deliveryType']; + if ($deliveries[$dtype] != null) { + if ($deliveries[$dtype] != $orderToUpdate->id_carrier) { + Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'orders` + SET `id_carrier` = \''.$deliveries[$dtype].'\' + WHERE `id_order` = '.(int) $order['externalId'] + ); + Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'order_carrier` + SET `id_carrier` = \''.$deliveries[$dtype].'\' + WHERE `id_order` = \''.$orderToUpdate->id.'\'' + ); + } + } + + /* + * check payment type + */ + $ptype = $order['paymentType']; + if ($payments[$ptype] != null) { + if ($payments[$ptype] != $orderToUpdate->payment) { + Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'orders` + SET `payment` = \''.$payments[$ptype].'\' + WHERE `id_order` = '.(int) $order['externalId'] + ); + Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'order_payment` + SET `payment_method` = \''.$payments[$ptype].'\' + WHERE `order_reference` = \''.$orderToUpdate->reference.'\'' + ); + + } + } + + /* + * check items + */ + + /* + * Clean deleted + */ + foreach ($order['items'] as $key => $item) { + if (isset($item['deleted']) && $item['deleted'] == true) { + Db::getInstance()->execute(' + DELETE FROM `'._DB_PREFIX_.'order_detail` + WHERE `id_order` = '. $orderToUpdate->id .' + AND `product_id` = '.$item['id'] + ); + + unset($order['items'][$key]); + } + } + + /* + * check quantity + */ + + foreach ($orderToUpdate->getProductsDetail() as $orderItem) { + foreach ($order['items'] as $key => $item) { + if ($item['offer']['externalId'] == $orderItem['product_id']) { + if (isset($item['quantity']) && $item['quantity'] != $orderItem['product_quantity']) { + Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'order_detail` + SET `product_quantity` = '.$item['quantity'].', + `product_quantity_in_stock` = '.$item['quantity'].' + WHERE `id_order_detail` = '.$orderItem['id_order_detail'] + ); + } + + unset($order['items'][$key]); + } + } + } + + /* + * check new items + */ + if (!empty($order['items'])) { + foreach ($order['items'] as $key => $newItem) { + $product = new Product((int) $newItem['offer']['externalId'], false, $default_lang); + $qty = $newItem['quantity']; + $product_list[] = array('product' =>$product, 'quantity' => $qty); + } + + + $query = 'INSERT `'._DB_PREFIX_.'order_detail` ( `id_order`, `id_order_invoice`, `id_shop`, `product_id`, `product_attribute_id`, `product_name`, `product_quantity`, `product_quantity_in_stock`, `product_price`, @@ -218,9 +415,9 @@ if (!empty($history->orders)) { VALUES'; - foreach ($product_list as $product) { - $query .= '(' - .(int) $newOrder->id.', + foreach ($product_list as $product) { + $query .= '(' + .(int) $orderToUpdate->id.', 0, '. $this->context->shop->id.', '.(int) $product['product']->id.', @@ -236,197 +433,45 @@ if (!empty($history->orders)) { '.$product['product']->price.', '.$product['product']->price.' ),'; + } + + Db::getInstance()->execute(rtrim($query, ',')); + unset($order['items'][$key]); } - Db::getInstance()->execute(rtrim($query, ',')); - - try { - $this->api->customersFixExternalIds($this->customerFix); - $this->api->ordesrFixExternalIds($this->orderFix); - } - catch (CurlException $e) { - error_log('fixExternalId: connection error', 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); - continue; - } - catch (InvalidJsonException $e) { - error_log('fixExternalId: ' . $e->getMessage(), 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); - continue; - } - - } else { - if (!in_array($order['id'], $toUpdate)) - { /* - * take last order update only + * Fix prices & discounts + * Discounts only for whole order */ + $orderDiscout = null; + $orderTotal = $order['summ']; - if ($order['paymentType'] != null && $order['deliveryType'] != null && $order['status'] != null) { - $orderToUpdate = new Order((int) $order['externalId']); - - /* - * check status - */ - $stype = $order['status']; - if ($statuses[$stype] != null) { - if ($statuses[$stype] != $orderToUpdate->current_state) { - Db::getInstance()->execute(' - UPDATE `'._DB_PREFIX_.'orders` - SET `current_state` = \''.$statuses[$stype].'\' - WHERE `id_order` = '.(int) $order['externalId']); - } + if (isset($order['discount']) && $order['discount'] > 0) { + if ($order['discount'] != $orderToUpdate->total_discounts) { + $orderDiscout = ($orderDiscout == null) ? $order['discount'] : $order['discount'] + $orderDiscout; } + } - /* - * check delivery type - */ - $dtype = $order['deliveryType']; - if ($deliveries[$dtype] != null) { - if ($deliveries[$dtype] != $orderToUpdate->id_carrier) { - Db::getInstance()->execute(' - UPDATE `'._DB_PREFIX_.'orders` - SET `id_carrier` = \''.$deliveries[$dtype].'\' - WHERE `id_order` = '.(int) $order['externalId']); - Db::getInstance()->execute(' - UPDATE `'._DB_PREFIX_.'order_carrier` - SET `id_carrier` = \''.$deliveries[$dtype].'\' - WHERE `id_order` = \''.$orderToUpdate->id.'\''); - } + if (isset($order['discountPercent']) && $order['discountPercent'] > 0) { + $percent = ($order['summ'] * $order['discountPercent'])/100; + if ($percent != $orderToUpdate->total_discounts) { + $orderDiscout = ($orderDiscout == null) ? $percent : $percent + $orderDiscout; } + } - /* - * check payment type - */ - $ptype = $order['paymentType']; - if ($payments[$ptype] != null) { - if ($payments[$ptype] != $orderToUpdate->payment) { - Db::getInstance()->execute(' - UPDATE `'._DB_PREFIX_.'orders` - SET `payment` = \''.$payments[$ptype].'\' - WHERE `id_order` = '.(int) $order['externalId']); - Db::getInstance()->execute(' - UPDATE `'._DB_PREFIX_.'order_payment` - SET `payment_method` = \''.$payments[$ptype].'\' - WHERE `order_reference` = \''.$orderToUpdate->reference.'\''); + $totalDiscount = ($orderDiscout == null) ? $orderToUpdate->total_discounts : $orderDiscout; - } - } - - /* - * check items - */ - - /* - * Clean deleted - */ - foreach ($order['items'] as $key => $item) { - if (isset($item['deleted']) && $item['deleted'] == true) { - Db::getInstance()->execute(' - DELETE FROM `'._DB_PREFIX_.'order_detail` - WHERE `id_order` = '. $orderToUpdate->id .' - AND `product_id` = '.$item['id']); - - unset($order['items'][$key]); - } - } - - /* - * check quantity - */ - - foreach ($orderToUpdate->getProductsDetail() as $orderItem) { - foreach ($order['items'] as $key => $item) { - if ($item['offer']['externalId'] == $orderItem['product_id']) { - if (isset($item['quantity']) && $item['quantity'] != $orderItem['product_quantity']) { - Db::getInstance()->execute(' - UPDATE `'._DB_PREFIX_.'order_detail` - SET `product_quantity` = '.$item['quantity'].', - `product_quantity_in_stock` = '.$item['quantity'].' - WHERE `id_order_detail` = '.$orderItem['id_order_detail']); - } - - unset($order['items'][$key]); - } - } - } - - /* - * check new items - */ - if (!empty($order['items'])) { - foreach ($order['items'] as $key => $newItem) { - $product = new Product((int) $newItem['offer']['externalId'], false, $this->default_lang); - $qty = $newItem['quantity']; - $product_list[] = array('product' =>$product, 'quantity' => $qty); - } - - - $query = 'INSERT `'._DB_PREFIX_.'order_detail` - ( - `id_order`, `id_order_invoice`, `id_shop`, `product_id`, `product_attribute_id`, - `product_name`, `product_quantity`, `product_quantity_in_stock`, `product_price`, - `product_reference`, `total_price_tax_excl`, `total_price_tax_incl`, - `unit_price_tax_excl`, `unit_price_tax_incl`, `original_product_price` - ) - - VALUES'; - - foreach ($product_list as $product) { - $query .= '(' - .(int) $orderToUpdate->id.', - 0, - '. $this->context->shop->id.', - '.(int) $product['product']->id.', - 0, - '.implode('', array('\'', $product['product']->name, '\'')).', - '.(int) $product['quantity'].', - '.(int) $product['quantity'].', - '.$product['product']->price.', - '.implode('', array('\'', $product['product']->reference, '\'')).', - '.$product['product']->price.', - '.$product['product']->price.', - '.$product['product']->price.', - '.$product['product']->price.', - '.$product['product']->price.' - ),'; - } - - Db::getInstance()->execute(rtrim($query, ',')); - unset($order['items'][$key]); - } - - /* - * Fix prices & discounts - * Discounts only for whole order - */ - $orderDiscout = null; - $orderTotal = $order['summ']; - - if (isset($order['discount']) && $order['discount'] > 0) { - if ($order['discount'] != $orderToUpdate->total_discounts) { - $orderDiscout = ($orderDiscout == null) ? $order['discount'] : $order['discount'] + $orderDiscout; - } - } - - if (isset($order['discountPercent']) && $order['discountPercent'] > 0) { - $percent = ($order['summ'] * $order['discountPercent'])/100; - if ($percent != $orderToUpdate->total_discounts) { - $orderDiscout = ($orderDiscout == null) ? $percent : $percent + $orderDiscout; - } - } - - $totalDiscount = ($orderDiscout == null) ? $orderToUpdate->total_discounts : $orderDiscout; - - if ($totalDiscount != $orderToUpdate->total_discounts || $orderTotal != $orderToUpdate->total_paid) { - Db::getInstance()->execute(' - UPDATE `'._DB_PREFIX_.'orders` - SET `total_discounts` = '.$totalDiscount.', - `total_discounts_tax_incl` = '.$totalDiscount.', - `total_discounts_tax_excl` = '.$totalDiscount.', - `total_paid` = '.$orderTotal.', - `total_paid_tax_incl` = '.$orderTotal.', - `total_paid_tax_excl` = '.$orderTotal.' - WHERE `id_order` = '.(int) $order['externalId']); - } + if ($totalDiscount != $orderToUpdate->total_discounts || $orderTotal != $orderToUpdate->total_paid) { + Db::getInstance()->execute(' + UPDATE `'._DB_PREFIX_.'orders` + SET `total_discounts` = '.$totalDiscount.', + `total_discounts_tax_incl` = '.$totalDiscount.', + `total_discounts_tax_excl` = '.$totalDiscount.', + `total_paid` = '.$orderTotal.', + `total_paid_tax_incl` = '.$orderTotal.', + `total_paid_tax_excl` = '.$orderTotal.' + WHERE `id_order` = '.(int) $order['externalId'] + ); } } } diff --git a/retailcrm/retailcrm.php b/retailcrm/retailcrm.php index 90b258b..a28c51b 100644 --- a/retailcrm/retailcrm.php +++ b/retailcrm/retailcrm.php @@ -297,89 +297,91 @@ class RetailCRM extends Module if (!empty($address)) { foreach ($address as $idx => $a) { - if (!strpos($a, ':')) { - $a = preg_replace('/_/', ' ', $a); - $a = preg_replace('/[\,\.]/', '', $a); - $addressFields[] = array( - 'type' => 'select', - 'label' => $this->l((string) ucfirst($a)), - 'name' => 'RETAILCRM_API_ADDR[' . $idx . ']', - 'required' => false, - 'options' => array( - 'query' => array( - array( - 'name' => '', - 'id_option' => '' + if (!in_array($a, array('vat_number', 'phone_mobile', 'company'))) { + if (!strpos($a, ':')) { + $a = preg_replace('/_/', ' ', $a); + $a = preg_replace('/[\,\.]/', '', $a); + $addressFields[] = array( + 'type' => 'select', + 'label' => $this->l((string) ucfirst($a)), + 'name' => 'RETAILCRM_API_ADDR[' . $idx . ']', + 'required' => false, + 'options' => array( + 'query' => array( + array( + 'name' => '', + 'id_option' => '' + ), + array( + 'name' => $this->l('FIRST_NAME'), + 'id_option' => 'first_name' + ), + array( + 'name' => $this->l('LAST_NAME'), + 'id_option' => 'last_name' + ), + array( + 'name' => $this->l('PHONE'), + 'id_option' => 'phone' + ), + array( + 'name' => $this->l('EMAIL'), + 'id_option' => 'email' + ), + array( + 'name' => $this->l('ADDRESS'), + 'id_option' => 'address' + ), + array( + 'name' => $this->l('COUNTRY'), + 'id_option' => 'country' + ), + array( + 'name' => $this->l('REGION'), + 'id_option' => 'region' + ), + array( + 'name' => $this->l('CITY'), + 'id_option' => 'city' + ), + array( + 'name' => $this->l('ZIP'), + 'id_option' => 'index' + ), + array( + 'name' => $this->l('STREET'), + 'id_option' => 'street' + ), + array( + 'name' => $this->l('BUILDING'), + 'id_option' => 'building' + ), + array( + 'name' => $this->l('FLAT'), + 'id_option' => 'flat' + ), + array( + 'name' => $this->l('INTERCOMCODE'), + 'id_option' => 'intercomcode' + ), + array( + 'name' => $this->l('FLOOR'), + 'id_option' => 'floor' + ), + array( + 'name' => $this->l('BLOCK'), + 'id_option' => 'block' + ), + array( + 'name' => $this->l('HOUSE'), + 'ID' => 'house' + ) ), - array( - 'name' => $this->l('FIRST_NAME'), - 'id_option' => 'first_name' - ), - array( - 'name' => $this->l('LAST_NAME'), - 'id_option' => 'last_name' - ), - array( - 'name' => $this->l('PHONE'), - 'id_option' => 'phone' - ), - array( - 'name' => $this->l('EMAIL'), - 'id_option' => 'email' - ), - array( - 'name' => $this->l('ADDRESS'), - 'id_option' => 'address' - ), - array( - 'name' => $this->l('COUNTRY'), - 'id_option' => 'country' - ), - array( - 'name' => $this->l('REGION'), - 'id_option' => 'region' - ), - array( - 'name' => $this->l('CITY'), - 'id_option' => 'city' - ), - array( - 'name' => $this->l('ZIP'), - 'id_option' => 'index' - ), - array( - 'name' => $this->l('STREET'), - 'id_option' => 'street' - ), - array( - 'name' => $this->l('BUILDING'), - 'id_option' => 'building' - ), - array( - 'name' => $this->l('FLAT'), - 'id_option' => 'flat' - ), - array( - 'name' => $this->l('INTERCOMCODE'), - 'id_option' => 'intercomcode' - ), - array( - 'name' => $this->l('FLOOR'), - 'id_option' => 'floor' - ), - array( - 'name' => $this->l('BLOCK'), - 'id_option' => 'block' - ), - array( - 'name' => $this->l('HOUSE'), - 'ID' => 'house' - ) - ), - 'id' => 'id_option', - 'name' => 'name' - ) - ); + 'id' => 'id_option', + 'name' => 'name' + ) + ); + } } } } From c0ea6c2a06e3bf2bdf27a7229b268e635acc6425 Mon Sep 17 00:00:00 2001 From: Alex Lushpai Date: Mon, 11 Jan 2016 18:02:20 +0300 Subject: [PATCH 13/17] customer, orders & pre-alpha archive export --- retailcrm/bootstrap.php | 99 +++ retailcrm/job/export.php | 99 +++ retailcrm/job/icml.php | 20 +- retailcrm/job/sync.php | 198 ++---- retailcrm/lib/CurlException.php | 5 + retailcrm/lib/InvalidJsonException.php | 5 + retailcrm/lib/RetailcrmApiClient.php | 811 +++++++++++++++++++++++++ retailcrm/lib/RetailcrmApiResponse.php | 122 ++++ retailcrm/lib/RetailcrmCatalog.php | 133 ++++ retailcrm/lib/RetailcrmHttpClient.php | 113 ++++ retailcrm/lib/RetailcrmIcml.php | 137 +++++ retailcrm/lib/RetailcrmProxy.php | 44 ++ retailcrm/lib/RetailcrmReferences.php | 190 ++++++ retailcrm/lib/RetailcrmService.php | 42 ++ retailcrm/retailcrm.php | 550 +---------------- retailcrm/version.1.6.php | 433 +++++++++++++ 16 files changed, 2304 insertions(+), 697 deletions(-) create mode 100644 retailcrm/bootstrap.php create mode 100644 retailcrm/job/export.php create mode 100644 retailcrm/lib/CurlException.php create mode 100644 retailcrm/lib/InvalidJsonException.php create mode 100644 retailcrm/lib/RetailcrmApiClient.php create mode 100644 retailcrm/lib/RetailcrmApiResponse.php create mode 100644 retailcrm/lib/RetailcrmCatalog.php create mode 100644 retailcrm/lib/RetailcrmHttpClient.php create mode 100644 retailcrm/lib/RetailcrmIcml.php create mode 100644 retailcrm/lib/RetailcrmProxy.php create mode 100644 retailcrm/lib/RetailcrmReferences.php create mode 100644 retailcrm/lib/RetailcrmService.php create mode 100644 retailcrm/version.1.6.php diff --git a/retailcrm/bootstrap.php b/retailcrm/bootstrap.php new file mode 100644 index 0000000..7c0cc34 --- /dev/null +++ b/retailcrm/bootstrap.php @@ -0,0 +1,99 @@ + + * @author Alex Lushpai + */ +class RetailcrmAutoloader +{ + /** + * File extension as a string. Defaults to ".php". + */ + protected static $fileExt = '.php'; + + /** + * The top level directory where recursion will begin. + * + */ + protected static $pathTop; + + /** + * Autoload function for registration with spl_autoload_register + * + * Looks recursively through project directory and loads class files based on + * filename match. + * + * @param string $className + */ + public static function loader($className) + { + $directory = new RecursiveDirectoryIterator(self::$pathTop); + $fileIterator = new RecursiveIteratorIterator($directory); + $filename = $className . self::$fileExt; + + foreach ($fileIterator as $file) { + if (strtolower($file->getFilename()) === strtolower($filename) && $file->isReadable()) { + include_once $file->getPathname(); + } + } + + } + + /** + * Sets the $fileExt property + * + * @param string $fileExt The file extension used for class files. Default is "php". + */ + public static function setFileExt($fileExt) + { + self::$fileExt = $fileExt; + } + + /** + * Sets the $path property + * + * @param string $path The path representing the top level where recursion should + * begin. Defaults to the current directory. + */ + public static function setPath($path) + { + self::$pathTop = $path; + } + +} + +RetailcrmAutoloader::setPath(realpath(dirname(__FILE__))); +RetailcrmAutoloader::setFileExt('.php'); +spl_autoload_register('RetailcrmAutoloader::loader'); diff --git a/retailcrm/job/export.php b/retailcrm/job/export.php new file mode 100644 index 0000000..0426bf4 --- /dev/null +++ b/retailcrm/job/export.php @@ -0,0 +1,99 @@ +getOrdersWithInformations(2); + +$delivery = json_decode(Configuration::get('RETAILCRM_API_DELIVERY')); +$payment = json_decode(Configuration::get('RETAILCRM_API_PAYMENT')); +$status = json_decode(Configuration::get('RETAILCRM_API_STATUS')); + +foreach ($records as $record) { + + $object = new Order($record['id_order']); + + if (Module::getInstanceByName('advancedcheckout') === false) { + $paymentType = $record['module']; + } else { + $paymentType = $record['payment']; + } + + $cart = new Cart($object->getCartIdStatic($record['id_order'])); + $addressCollection = $cart->getAddressCollection(); + $address = array_shift($addressCollection); + + $order = array( + 'externalId' => $record['id_order'], + 'createdAt' => $record['date_add'], + 'status' => $record['current_state'] == 0 ? 'new' : $status->$record['current_state'], + 'firstName' => $record['firstname'], + 'lastName' => $record['lastname'], + 'email' => $record['email'], + 'phone' => $address->phone, + 'delivery' => array( + 'code' => $delivery->$record['id_carrier'], + 'cost' => $record['total_shipping_tax_incl'], + 'address' => array( + 'index' => $address->postcode, + 'city' => $address->city, + 'street' => sprintf("%s %s", $address->address1, $address->address2) + ) + ), + 'paymentType' => $payment->$paymentType + ); + + $products = $object->getProducts(); + + foreach($products as $product) { + $item = array( + 'productId' => $product['product_id'], + 'productName' => $product['product_name'], + 'quantity' => $product['product_quantity'], + 'initialPrice' => round($product['product_price'], 2), + 'purchasePrice' => round($product['purchase_supplier_price'], 2) + ); + + $order['items'][] = $item; + } + + if ($record['id_customer']) { + $order['customer']['externalId'] = $record['id_customer']; + + $customer = new Customer($record['id_customer']); + $customerCRM = array( + 'externalId' => $customer->id, + 'firstName' => $customer->firstname, + 'lastname' => $customer->lastname, + 'email' => $customer->email, + 'phones' => array(array('number' => $address->phone)), + 'createdAt' => $customer->date_add, + 'address' => array( + 'index' => $address->postcode, + 'city' => $address->city, + 'street' => sprintf("%s %s", $address->address1, $address->address2) + ) + ); + + $customers[$customer->id] = $customerCRM; + } + + $orders[] = $order; +} + +var_dump(count($customers)); +var_dump(count($orders)); diff --git a/retailcrm/job/icml.php b/retailcrm/job/icml.php index ec0ddec..9cf6da5 100644 --- a/retailcrm/job/icml.php +++ b/retailcrm/job/icml.php @@ -1,21 +1,11 @@ getData(); -$icml = new Icml( - Configuration::get('PS_SHOP_NAME'), - _PS_ROOT_DIR_ . '/retailcrm.xml' -); - +$icml = new RetailcrmIcml(Configuration::get('PS_SHOP_NAME'), _PS_ROOT_DIR_ . '/retailcrm.xml'); $icml->generate($data[0], $data[1]); diff --git a/retailcrm/job/sync.php b/retailcrm/job/sync.php index d567a50..07e50c5 100644 --- a/retailcrm/job/sync.php +++ b/retailcrm/job/sync.php @@ -1,13 +1,8 @@ ordersHistory(new DateTime($startFrom)); -} -catch (CurlException $e) { - error_log('orderHistory: connection error', 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); -} -catch (InvalidJsonException $e) { - error_log('orderHistory: ' . $e->getMessage(), 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); -} +$history = $api->ordersHistory(new DateTime($startFrom)); -/* - * store recieved data into shop database -*/ -if (count($history->orders) > 0) { +if ($history->isSuccess() && count($history->orders) > 0) { - /* - * Customer object. Will be used for further updates. - */ - - $statuses = array_filter( - json_decode( - Configuration::get('RETAILCRM_API_STATUS'), true - ) - ); - - $statuses = array_flip($statuses); - - $deliveries = array_filter( - json_decode( - Configuration::get('RETAILCRM_API_DELIVERY'), true - ) - ); - - $deliveries = array_flip($deliveries); - - $payments = array_filter( - json_decode( - Configuration::get('RETAILCRM_API_PAYMENT'), true - ) - ); - - $payments = array_flip($payments); + $statuses = array_flip(array_filter(json_decode(Configuration::get('RETAILCRM_API_STATUS'), true))); + $deliveries = array_flip(array_filter(json_decode(Configuration::get('RETAILCRM_API_DELIVERY'), true))); + $payments = array_flip(array_filter(json_decode(Configuration::get('RETAILCRM_API_PAYMENT'), true))); foreach ($history->orders as $order) { if (!array_key_exists('externalId', $order)) { - /* - * create customer if not exist - */ $customer = new Customer(); $customer->getByEmail($order['customer']['email']); - if (!array_key_exists('externalId', $order['customer'])) { - if (Validate::isEmail($order['customer']['email'])) { + if ( + !array_key_exists('externalId', $order['customer']) && + Validate::isEmail($order['customer']['email']) + ) { + if (!$customer->id) + { + $customer->firstname = $order['customer']['firstName']; + $customer->lastname = $order['customer']['lastName']; + $customer->email = $order['customer']['email']; + $customer->passwd = substr(str_shuffle(strtolower(sha1(rand() . time()))),0, 5); - if (!$customer->id) - { - $customer->firstname = $order['customer']['firstName']; - $customer->lastname = $order['customer']['lastName']; - $customer->email = $order['customer']['email']; - $customer->passwd = substr(str_shuffle(strtolower(sha1(rand() . time()))),0, 5); - - if($customer->add()) { - - /* - * create customer address for delivery data - */ - - $customer->getByEmail($order['customer']['email']); - $customer_id = $customer->id; - - $address = new Address(); - $address->id_customer = $customer->id; - $address->id_country = $default_country; - $address->lastname = $customer->lastname; - $address->firstname = $customer->firstname; - $address->alias = 'default'; - $address->postcode = $customer['address']['index']; - $address->city = $customer['address']['city']; - $address->address1 = $customer['address']['text']; - $address->phone = $customer['phones'][0]['number']; - - $address->add(); - - /* - * store address record id for handle order data - */ - $addr = $customer->getAddresses($default_lang); - $address_id = $addr[0]['id_address']; - } - } else { - $addresses = $customer->getAddresses($default_lang); - $address_id = $addresses[0]['id_address']; + if($customer->add()) { + $customer->getByEmail($order['customer']['email']); $customer_id = $customer->id; - } - /* - * collect customer ids for single fix request - */ - array_push( - $customerFix, - array( - 'id' => $order['customer']['id'], - 'externalId' => $customer_id - ) - ); + $address = new Address(); + $address->id_customer = $customer->id; + $address->id_country = $default_country; + $address->lastname = $customer->lastname; + $address->firstname = $customer->firstname; + $address->alias = 'default'; + $address->postcode = $customer['address']['index']; + $address->city = $customer['address']['city']; + $address->address1 = $customer['address']['text']; + $address->phone = $customer['phones'][0]['number']; + + $address->add(); + $addr = $customer->getAddresses($default_lang); + $address_id = $addr[0]['id_address']; + } + } else { + $addresses = $customer->getAddresses($default_lang); + $address_id = $addresses[0]['id_address']; + $customer_id = $customer->id; } + + array_push( + $customerFix, + array( + 'id' => $order['customer']['id'], + 'externalId' => $customer_id + ) + ); } else { $addresses = $customer->getAddresses($default_lang); $address_id = $addresses[0]['id_address']; @@ -185,7 +125,6 @@ if (count($history->orders) > 0) { /* * Create order */ - $newOrder = new Order(); $newOrder->id_address_delivery = (int) $address_id; $newOrder->id_address_invoice = (int) $address_id; @@ -270,34 +209,12 @@ if (count($history->orders) > 0) { Db::getInstance()->execute(rtrim($query, ',')); - try { - $this->api->customersFixExternalIds($customerFix); - } - catch (CurlException $e) { - error_log('customersFixExternalId: connection error', 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); - continue; - } - catch (InvalidJsonException $e) { - error_log('customersFixExternalId: ' . $e->getMessage(), 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); - continue; - } - - try { - $this->api->ordersFixExternalIds($orderFix); - } - catch (CurlException $e) { - error_log('ordersFixExternalId: connection error', 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); - continue; - } - catch (InvalidJsonException $e) { - error_log('ordersFixExternalId: ' . $e->getMessage(), 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); - continue; - } + $this->api->customersFixExternalIds($customerFix); + $this->api->ordersFixExternalIds($orderFix); } else { /* * take last order update only */ - if ($order['paymentType'] != null && $order['deliveryType'] != null && $order['status'] != null) { $orderToUpdate = new Order((int) $order['externalId']); @@ -355,11 +272,7 @@ if (count($history->orders) > 0) { } /* - * check items - */ - - /* - * Clean deleted + * Clean deleted items */ foreach ($order['items'] as $key => $item) { if (isset($item['deleted']) && $item['deleted'] == true) { @@ -374,9 +287,8 @@ if (count($history->orders) > 0) { } /* - * check quantity + * Check items quantity */ - foreach ($orderToUpdate->getProductsDetail() as $orderItem) { foreach ($order['items'] as $key => $item) { if ($item['offer']['externalId'] == $orderItem['product_id']) { @@ -395,7 +307,7 @@ if (count($history->orders) > 0) { } /* - * check new items + * Check new items */ if (!empty($order['items'])) { foreach ($order['items'] as $key => $newItem) { @@ -483,4 +395,4 @@ if (count($history->orders) > 0) { Configuration::updateValue('RETAILCRM_LAST_SYNC', $history->generatedAt); } else { return 'Nothing to sync'; -} \ No newline at end of file +} diff --git a/retailcrm/lib/CurlException.php b/retailcrm/lib/CurlException.php new file mode 100644 index 0000000..2ab00f5 --- /dev/null +++ b/retailcrm/lib/CurlException.php @@ -0,0 +1,5 @@ +client = new RetailcrmHttpClient($url, array('apiKey' => $apiKey)); + $this->siteCode = $site; + } + + /** + * Create a order + * + * @param array $order + * @param string $site (default: null) + * @return RetailcrmApiResponse + */ + public function ordersCreate(array $order, $site = null) + { + if (!sizeof($order)) { + throw new InvalidArgumentException('Parameter `order` must contains a data'); + } + + return $this->client->makeRequest("/orders/create", RetailcrmHttpClient::METHOD_POST, $this->fillSite($site, array( + 'order' => json_encode($order) + ))); + } + + /** + * Edit a order + * + * @param array $order + * @param string $by + * @param string $site (default: null) + * @return RetailcrmApiResponse + */ + public function ordersEdit(array $order, $by = 'externalId', $site = null) + { + if (!sizeof($order)) { + throw new InvalidArgumentException('Parameter `order` must contains a data'); + } + + $this->checkIdParameter($by); + + if (!isset($order[$by])) { + throw new InvalidArgumentException(sprintf('Order array must contain the "%s" parameter.', $by)); + } + + return $this->client->makeRequest( + "/orders/" . $order[$by] . "/edit", + RetailcrmHttpClient::METHOD_POST, + $this->fillSite($site, array( + 'order' => json_encode($order), + 'by' => $by, + )) + ); + } + + /** + * Upload array of the orders + * + * @param array $orders + * @param string $site (default: null) + * @return RetailcrmApiResponse + */ + public function ordersUpload(array $orders, $site = null) + { + if (!sizeof($orders)) { + throw new InvalidArgumentException('Parameter `orders` must contains array of the orders'); + } + + return $this->client->makeRequest("/orders/upload", RetailcrmHttpClient::METHOD_POST, $this->fillSite($site, array( + 'orders' => json_encode($orders), + ))); + } + + /** + * Get order by id or externalId + * + * @param string $id + * @param string $by (default: 'externalId') + * @param string $site (default: null) + * @return RetailcrmApiResponse + */ + public function ordersGet($id, $by = 'externalId', $site = null) + { + $this->checkIdParameter($by); + + return $this->client->makeRequest("/orders/$id", RetailcrmHttpClient::METHOD_GET, $this->fillSite($site, array( + 'by' => $by + ))); + } + + /** + * Returns a orders history + * + * @param DateTime $startDate (default: null) + * @param DateTime $endDate (default: null) + * @param int $limit (default: 100) + * @param int $offset (default: 0) + * @param bool $skipMyChanges (default: true) + * @return RetailcrmApiResponse + */ + public function ordersHistory( + DateTime $startDate = null, + DateTime $endDate = null, + $limit = 100, + $offset = 0, + $skipMyChanges = true + ) { + $parameters = array(); + + if ($startDate) { + $parameters['startDate'] = $startDate->format('Y-m-d H:i:s'); + } + if ($endDate) { + $parameters['endDate'] = $endDate->format('Y-m-d H:i:s'); + } + if ($limit) { + $parameters['limit'] = (int) $limit; + } + if ($offset) { + $parameters['offset'] = (int) $offset; + } + if ($skipMyChanges) { + $parameters['skipMyChanges'] = (bool) $skipMyChanges; + } + + return $this->client->makeRequest('/orders/history', RetailcrmHttpClient::METHOD_GET, $parameters); + } + + /** + * Returns filtered orders list + * + * @param array $filter (default: array()) + * @param int $page (default: null) + * @param int $limit (default: null) + * @return RetailcrmApiResponse + */ + public function ordersList(array $filter = array(), $page = null, $limit = null) + { + $parameters = array(); + + if (sizeof($filter)) { + $parameters['filter'] = $filter; + } + if (null !== $page) { + $parameters['page'] = (int) $page; + } + if (null !== $limit) { + $parameters['limit'] = (int) $limit; + } + + return $this->client->makeRequest('/orders', RetailcrmHttpClient::METHOD_GET, $parameters); + } + + /** + * Returns statuses of the orders + * + * @param array $ids (default: array()) + * @param array $externalIds (default: array()) + * @return RetailcrmApiResponse + */ + public function ordersStatuses(array $ids = array(), array $externalIds = array()) + { + $parameters = array(); + + if (sizeof($ids)) { + $parameters['ids'] = $ids; + } + if (sizeof($externalIds)) { + $parameters['externalIds'] = $externalIds; + } + + return $this->client->makeRequest('/orders/statuses', RetailcrmHttpClient::METHOD_GET, $parameters); + } + + /** + * Save order IDs' (id and externalId) association in the CRM + * + * @param array $ids + * @return RetailcrmApiResponse + */ + public function ordersFixExternalIds(array $ids) + { + if (!sizeof($ids)) { + throw new InvalidArgumentException('Method parameter must contains at least one IDs pair'); + } + + return $this->client->makeRequest("/orders/fix-external-ids", RetailcrmHttpClient::METHOD_POST, array( + 'orders' => json_encode($ids), + )); + } + + /** + * Get orders assembly history + * + * @param array $filter (default: array()) + * @param int $page (default: null) + * @param int $limit (default: null) + * @return RetailcrmApiResponse + */ + public function ordersPacksHistory(array $filter = array(), $page = null, $limit = null) + { + $parameters = array(); + + if (sizeof($filter)) { + $parameters['filter'] = $filter; + } + if (null !== $page) { + $parameters['page'] = (int) $page; + } + if (null !== $limit) { + $parameters['limit'] = (int) $limit; + } + + return $this->client->makeRequest('/orders/packs/history', RetailcrmHttpClient::METHOD_GET, $parameters); + } + + /** + * Create a customer + * + * @param array $customer + * @param string $site (default: null) + * @return RetailcrmApiResponse + */ + public function customersCreate(array $customer, $site = null) + { + if (!sizeof($customer)) { + throw new InvalidArgumentException('Parameter `customer` must contains a data'); + } + + return $this->client->makeRequest("/customers/create", RetailcrmHttpClient::METHOD_POST, $this->fillSite($site, array( + 'customer' => json_encode($customer) + ))); + } + + /** + * Edit a customer + * + * @param array $customer + * @param string $by (default: 'externalId') + * @param string $site (default: null) + * @return RetailcrmApiResponse + */ + public function customersEdit(array $customer, $by = 'externalId', $site = null) + { + if (!sizeof($customer)) { + throw new InvalidArgumentException('Parameter `customer` must contains a data'); + } + + $this->checkIdParameter($by); + + if (!isset($customer[$by])) { + throw new InvalidArgumentException(sprintf('Customer array must contain the "%s" parameter.', $by)); + } + + return $this->client->makeRequest( + "/customers/" . $customer[$by] . "/edit", + RetailcrmHttpClient::METHOD_POST, + $this->fillSite( + $site, + array( + 'customer' => json_encode($customer), + 'by' => $by + ) + ) + ); + } + + /** + * Upload array of the customers + * + * @param array $customers + * @param string $site (default: null) + * @return RetailcrmApiResponse + */ + public function customersUpload(array $customers, $site = null) + { + if (!sizeof($customers)) { + throw new InvalidArgumentException('Parameter `customers` must contains array of the customers'); + } + + return $this->client->makeRequest("/customers/upload", RetailcrmHttpClient::METHOD_POST, $this->fillSite($site, array( + 'customers' => json_encode($customers), + ))); + } + + /** + * Get customer by id or externalId + * + * @param string $id + * @param string $by (default: 'externalId') + * @param string $site (default: null) + * @return RetailcrmApiResponse + */ + public function customersGet($id, $by = 'externalId', $site = null) + { + $this->checkIdParameter($by); + + return $this->client->makeRequest("/customers/$id", RetailcrmHttpClient::METHOD_GET, $this->fillSite($site, array( + 'by' => $by + ))); + } + + /** + * Returns filtered customers list + * + * @param array $filter (default: array()) + * @param int $page (default: null) + * @param int $limit (default: null) + * @return RetailcrmApiResponse + */ + public function customersList(array $filter = array(), $page = null, $limit = null) + { + $parameters = array(); + + if (sizeof($filter)) { + $parameters['filter'] = $filter; + } + if (null !== $page) { + $parameters['page'] = (int) $page; + } + if (null !== $limit) { + $parameters['limit'] = (int) $limit; + } + + return $this->client->makeRequest('/customers', RetailcrmHttpClient::METHOD_GET, $parameters); + } + + /** + * Save customer IDs' (id and externalId) association in the CRM + * + * @param array $ids + * @return RetailcrmApiResponse + */ + public function customersFixExternalIds(array $ids) + { + if (!sizeof($ids)) { + throw new InvalidArgumentException('Method parameter must contains at least one IDs pair'); + } + + return $this->client->makeRequest("/customers/fix-external-ids", RetailcrmHttpClient::METHOD_POST, array( + 'customers' => json_encode($ids), + )); + } + + /** + * Get purchace prices & stock balance + * + * @param array $filter (default: array()) + * @param int $page (default: null) + * @param int $limit (default: null) + * @param string $site (default: null) + * @return RetailcrmApiResponse + */ + public function storeInventories(array $filter = array(), $page = null, $limit = null, $site = null) + { + $parameters = array(); + + if (sizeof($filter)) { + $parameters['filter'] = $filter; + } + if (null !== $page) { + $parameters['page'] = (int) $page; + } + if (null !== $limit) { + $parameters['limit'] = (int) $limit; + } + + return $this->client->makeRequest('/store/inventories', RetailcrmHttpClient::METHOD_GET, $this->fillSite($site, $parameters)); + } + + /** + * Upload store inventories + * + * @param array $offers + * @param string $site (default: null) + * @return RetailcrmApiResponse + */ + public function storeInventoriesUpload(array $offers, $site = null) + { + if (!sizeof($offers)) { + throw new InvalidArgumentException('Parameter `offers` must contains array of the customers'); + } + + return $this->client->makeRequest( + "/store/inventories/upload", + RetailcrmHttpClient::METHOD_POST, + $this->fillSite($site, array('offers' => json_encode($offers))) + ); + } + + /** + * Returns deliveryServices list + * + * @return RetailcrmApiResponse + */ + public function deliveryServicesList() + { + return $this->client->makeRequest('/reference/delivery-services', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Returns deliveryTypes list + * + * @return RetailcrmApiResponse + */ + public function deliveryTypesList() + { + return $this->client->makeRequest('/reference/delivery-types', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Returns orderMethods list + * + * @return RetailcrmApiResponse + */ + public function orderMethodsList() + { + return $this->client->makeRequest('/reference/order-methods', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Returns orderTypes list + * + * @return RetailcrmApiResponse + */ + public function orderTypesList() + { + return $this->client->makeRequest('/reference/order-types', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Returns paymentStatuses list + * + * @return RetailcrmApiResponse + */ + public function paymentStatusesList() + { + return $this->client->makeRequest('/reference/payment-statuses', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Returns paymentTypes list + * + * @return RetailcrmApiResponse + */ + public function paymentTypesList() + { + return $this->client->makeRequest('/reference/payment-types', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Returns productStatuses list + * + * @return RetailcrmApiResponse + */ + public function productStatusesList() + { + return $this->client->makeRequest('/reference/product-statuses', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Returns statusGroups list + * + * @return RetailcrmApiResponse + */ + public function statusGroupsList() + { + return $this->client->makeRequest('/reference/status-groups', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Returns statuses list + * + * @return RetailcrmApiResponse + */ + public function statusesList() + { + return $this->client->makeRequest('/reference/statuses', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Returns sites list + * + * @return RetailcrmApiResponse + */ + public function sitesList() + { + return $this->client->makeRequest('/reference/sites', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Returns stores list + * + * @return RetailcrmApiResponse + */ + public function storesList() + { + return $this->client->makeRequest('/reference/stores', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Edit deliveryService + * + * @param array $data delivery service data + * @return RetailcrmApiResponse + */ + public function deliveryServicesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/delivery-services/' . $data['code'] . '/edit', + RetailcrmHttpClient::METHOD_POST, + array( + 'deliveryService' => json_encode($data) + ) + ); + } + + /** + * Edit deliveryType + * + * @param array $data delivery type data + * @return RetailcrmApiResponse + */ + public function deliveryTypesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/delivery-types/' . $data['code'] . '/edit', + RetailcrmHttpClient::METHOD_POST, + array( + 'deliveryType' => json_encode($data) + ) + ); + } + + /** + * Edit orderMethod + * + * @param array $data order method data + * @return RetailcrmApiResponse + */ + public function orderMethodsEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/order-methods/' . $data['code'] . '/edit', + RetailcrmHttpClient::METHOD_POST, + array( + 'orderMethod' => json_encode($data) + ) + ); + } + + /** + * Edit orderType + * + * @param array $data order type data + * @return RetailcrmApiResponse + */ + public function orderTypesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/order-types/' . $data['code'] . '/edit', + RetailcrmHttpClient::METHOD_POST, + array( + 'orderType' => json_encode($data) + ) + ); + } + + /** + * Edit paymentStatus + * + * @param array $data payment status data + * @return RetailcrmApiResponse + */ + public function paymentStatusesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/payment-statuses/' . $data['code'] . '/edit', + RetailcrmHttpClient::METHOD_POST, + array( + 'paymentStatus' => json_encode($data) + ) + ); + } + + /** + * Edit paymentType + * + * @param array $data payment type data + * @return RetailcrmApiResponse + */ + public function paymentTypesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/payment-types/' . $data['code'] . '/edit', + RetailcrmHttpClient::METHOD_POST, + array( + 'paymentType' => json_encode($data) + ) + ); + } + + /** + * Edit productStatus + * + * @param array $data product status data + * @return RetailcrmApiResponse + */ + public function productStatusesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/product-statuses/' . $data['code'] . '/edit', + RetailcrmHttpClient::METHOD_POST, + array( + 'productStatus' => json_encode($data) + ) + ); + } + + /** + * Edit order status + * + * @param array $data status data + * @return RetailcrmApiResponse + */ + public function statusesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/statuses/' . $data['code'] . '/edit', + RetailcrmHttpClient::METHOD_POST, + array( + 'status' => json_encode($data) + ) + ); + } + + /** + * Edit site + * + * @param array $data site data + * @return RetailcrmApiResponse + */ + public function sitesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + return $this->client->makeRequest( + '/reference/sites/' . $data['code'] . '/edit', + RetailcrmHttpClient::METHOD_POST, + array( + 'site' => json_encode($data) + ) + ); + } + + /** + * Edit store + * + * @param array $data site data + * @return RetailcrmApiResponse + */ + public function storesEdit(array $data) + { + if (!isset($data['code'])) { + throw new InvalidArgumentException('Data must contain "code" parameter.'); + } + + if (!isset($data['name'])) { + throw new InvalidArgumentException('Data must contain "name" parameter.'); + } + + return $this->client->makeRequest( + '/reference/stores/' . $data['code'] . '/edit', + RetailcrmHttpClient::METHOD_POST, + array( + 'store' => json_encode($data) + ) + ); + } + + /** + * Update CRM basic statistic + * + * @return RetailcrmApiResponse + */ + public function statisticUpdate() + { + return $this->client->makeRequest('/statistic/update', RetailcrmHttpClient::METHOD_GET); + } + + /** + * Return current site + * + * @return string + */ + public function getSite() + { + return $this->siteCode; + } + + /** + * Set site + * + * @param string $site + * @return void + */ + public function setSite($site) + { + $this->siteCode = $site; + } + + /** + * Check ID parameter + * + * @param string $by + * @return bool + */ + protected function checkIdParameter($by) + { + $allowedForBy = array('externalId', 'id'); + if (!in_array($by, $allowedForBy)) { + throw new InvalidArgumentException(sprintf( + 'Value "%s" for parameter "by" is not valid. Allowed values are %s.', + $by, + implode(', ', $allowedForBy) + )); + } + + return true; + } + + /** + * Fill params by site value + * + * @param string $site + * @param array $params + * @return array + */ + protected function fillSite($site, array $params) + { + if ($site) { + $params['site'] = $site; + } elseif ($this->siteCode) { + $params['site'] = $this->siteCode; + } + + return $params; + } +} diff --git a/retailcrm/lib/RetailcrmApiResponse.php b/retailcrm/lib/RetailcrmApiResponse.php new file mode 100644 index 0000000..bba91db --- /dev/null +++ b/retailcrm/lib/RetailcrmApiResponse.php @@ -0,0 +1,122 @@ +statusCode = (int) $statusCode; + + if (!empty($responseBody)) { + $response = json_decode($responseBody, true); + + if (!$response && JSON_ERROR_NONE !== ($error = json_last_error())) { + throw new InvalidJsonException( + "Invalid JSON in the API response body. Error code #$error", + $error + ); + } + + $this->response = $response; + } + } + + /** + * Return HTTP response status code + * + * @return int + */ + public function getStatusCode() + { + return $this->statusCode; + } + + /** + * HTTP request was successful + * + * @return bool + */ + public function isSuccessful() + { + return $this->statusCode < 400; + } + + /** + * Allow to access for the property throw class method + * + * @param string $name + * @return mixed + */ + public function __call($name, $arguments) + { + // convert getSomeProperty to someProperty + $propertyName = strtolower(substr($name, 3, 1)) . substr($name, 4); + + if (!isset($this->response[$propertyName])) { + throw new InvalidArgumentException("Method \"$name\" not found"); + } + + return $this->response[$propertyName]; + } + + /** + * Allow to access for the property throw object property + * + * @param string $name + * @return mixed + */ + public function __get($name) + { + if (!isset($this->response[$name])) { + throw new InvalidArgumentException("Property \"$name\" not found"); + } + + return $this->response[$name]; + } + + /** + * @param mixed $offset + * @param mixed $value + */ + public function offsetSet($offset, $value) + { + throw new BadMethodCallException('This activity not allowed'); + } + + /** + * @param mixed $offset + */ + public function offsetUnset($offset) + { + throw new BadMethodCallException('This call not allowed'); + } + + /** + * @param mixed $offset + * @return bool + */ + public function offsetExists($offset) + { + return isset($this->response[$offset]); + } + + /** + * @param mixed $offset + * @return mixed + */ + public function offsetGet($offset) + { + if (!isset($this->response[$offset])) { + throw new InvalidArgumentException("Property \"$offset\" not found"); + } + + return $this->response[$offset]; + } +} diff --git a/retailcrm/lib/RetailcrmCatalog.php b/retailcrm/lib/RetailcrmCatalog.php new file mode 100644 index 0000000..a1808b1 --- /dev/null +++ b/retailcrm/lib/RetailcrmCatalog.php @@ -0,0 +1,133 @@ +default_lang = (int) Configuration::get('PS_LANG_DEFAULT'); + $this->default_currency = (int) Configuration::get('PS_CURRENCY_DEFAULT'); + $this->default_country = (int) Configuration::get('PS_COUNTRY_DEFAULT'); + } + + public function getData() + { + + $id_lang = (int) Configuration::get('PS_LANG_DEFAULT'); + $currency = new Currency(Configuration::get('PS_CURRENCY_DEFAULT')); + $shop_url = (Configuration::get('PS_SSL_ENABLED') ? _PS_BASE_URL_SSL_ : _PS_BASE_URL_); + + $items = array(); + $categories = array(); + + if ($currency->iso_code == 'RUB') { + $currency->iso_code = 'RUR'; + } + + $currencies = Currency::getCurrencies(); + + $types = Category::getCategories($id_lang, true, false); + foreach ($types AS $category) { + $categories[] = array( + 'id' => $category['id_category'], + 'parentId' => $category['id_parent'], + 'name' => $category['name'] + ); + } + + $products = Product::getProducts($id_lang, 0, 0, 'name', 'asc'); + + foreach ($products AS $product) { + $category = $product['id_category_default']; + + if ($category == Configuration::get('PS_HOME_CATEGORY')) { + $temp_categories = Product::getProductCategories($product['id_product']); + + foreach ($temp_categories AS $category) { + if ($category != Configuration::get('PS_HOME_CATEGORY')) + break; + } + + if ($category == Configuration::get('PS_HOME_CATEGORY')) { + continue; + } + } + + $link = new Link(); + $cover = Image::getCover($product['id_product']); + + $picture = 'http://' . $link->getImageLink($product['link_rewrite'], $product['id_product'] . '-' . $cover['id_image'], 'large_default'); + if (!(substr($picture, 0, strlen($shop_url)) === $shop_url)) { + $picture = rtrim($shop_url, "/") . $picture; + } + + $crewrite = Category::getLinkRewrite($product['id_category_default'], $id_lang); + $url = $link->getProductLink($product['id_product'], $product['link_rewrite'], $crewrite); + $version = substr(_PS_VERSION_, 0, 3); + + if ($version == "1.3") { + $available_for_order = $product['active'] && $product['quantity']; + $quantity = $product['quantity']; + } else { + $prod = new Product($product['id_product']); + $available_for_order = $product['active'] && $product['available_for_order'] && $prod->checkQty(1); + $quantity = (int) StockAvailable::getQuantityAvailableByProduct($prod->id); + } + + $item = array( + 'id' => $product['id_product'], + 'productId' => $product['id_product'], + 'productActivity' => ($available_for_order) ? 'Y' : 'N', + 'name' => htmlspecialchars(strip_tags($product['name'])), + 'productName' => htmlspecialchars(strip_tags($product['name'])), + 'categoryId' => array($category), + 'picture' => $picture, + 'url' => $url, + 'quantity' => $quantity > 0 ? $quantity : 0 + ); + + if (!empty($product['wholesale_price'])) { + $item['purchasePrice'] = round($product['wholesale_price'], 2); + } + + $item['price'] = !empty($product['rate']) + ? round($product['price'], 2) + (round($product['price'], 2) * $product['rate'] / 100) + : round($product['price'], 2) + ; + + + if (!empty($product['manufacturer_name'])) { + $item['vendor'] = $product['manufacturer_name']; + } + + if (!empty($product['reference'])) { + $item['article'] = htmlspecialchars($product['reference']); + } + + $weight = round($product['weight'], 2); + + if (!empty($weight)) { + $item['weight'] = $weight; + } + + $width = round($product['width'], 2); + $height = round($product['height'], 2); + $depth = round($product['depth'], 2); + + if (!empty($width)) { + if (!empty($height)) { + if (!empty($depth)) { + $item['size'] = implode('x', array($width, $height, $depth)); + } else { + $item['size'] = implode('x', array($width, $height)); + } + } + } + + $items[] = $item; + } + + return array($categories, $items); + } + +} diff --git a/retailcrm/lib/RetailcrmHttpClient.php b/retailcrm/lib/RetailcrmHttpClient.php new file mode 100644 index 0000000..6f74d16 --- /dev/null +++ b/retailcrm/lib/RetailcrmHttpClient.php @@ -0,0 +1,113 @@ +url = $url; + $this->defaultParameters = $defaultParameters; + $this->retry = 0; + } + + /** + * Make HTTP request + * + * @param string $path + * @param string $method (default: 'GET') + * @param array $parameters (default: array()) + * @param int $timeout + * @param bool $verify + * @param bool $debug + * @return RetailcrmApiResponse + */ + public function makeRequest( + $path, + $method, + array $parameters = array(), + $timeout = 30, + $verify = false, + $debug = false + ) { + $allowedMethods = array(self::METHOD_GET, self::METHOD_POST); + if (!in_array($method, $allowedMethods)) { + throw new InvalidArgumentException(sprintf( + 'Method "%s" is not valid. Allowed methods are %s', + $method, + implode(', ', $allowedMethods) + )); + } + + $parameters = array_merge($this->defaultParameters, $parameters); + + $url = $this->url . $path; + + if (self::METHOD_GET === $method && sizeof($parameters)) { + $url .= '?' . http_build_query($parameters, '', '&'); + } + + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); + curl_setopt($ch, CURLOPT_FAILONERROR, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $verify); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $verify); + + if (!$debug) { + curl_setopt($ch, CURLOPT_TIMEOUT, (int) $timeout); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, (int) $timeout); + } else { + curl_setopt($ch, CURLOPT_TIMEOUT_MS, (int) $timeout + ($this->retry * 2000)); + } + + if (self::METHOD_POST === $method) { + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $parameters); + } + + $responseBody = curl_exec($ch); + $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $errno = curl_errno($ch); + $error = curl_error($ch); + + curl_close($ch); + + if ($errno && in_array($errno, array(6, 7, 28, 34, 35)) && $this->retry < 3) { + $errno = null; + $error = null; + $this->retry += 1; + $this->makeRequest( + $path, + $method, + $parameters, + $timeout, + $verify, + $debug + ); + } + + if ($errno) { + throw new CurlException($error, $errno); + } + + return new RetailcrmApiResponse($statusCode, $responseBody); + } + + public function getRetry() + { + return $this->retry; + } +} diff --git a/retailcrm/lib/RetailcrmIcml.php b/retailcrm/lib/RetailcrmIcml.php new file mode 100644 index 0000000..5b5f950 --- /dev/null +++ b/retailcrm/lib/RetailcrmIcml.php @@ -0,0 +1,137 @@ +shop = $shop; + $this->file = $file; + + $this->properties = array( + 'name', + 'productName', + 'price', + 'purchasePrice', + 'vendor', + 'picture', + 'url', + 'xmlId', + 'productActivity' + ); + + $this->params = array( + 'article' => 'Артикул', + 'color' => 'Цвет', + 'weight' => 'Вес', + 'size' => 'Размер', + 'tax' => 'Наценка' + ); + } + + public function generate($categories, $offers) + { + $string = ' + + + ' . $this->shop . ' + + + + + '; + + $xml = new SimpleXMLElement( + $string, LIBXML_NOENT | LIBXML_NOCDATA | LIBXML_COMPACT | LIBXML_PARSEHUGE + ); + + $this->dd = new DOMDocument(); + $this->dd->preserveWhiteSpace = false; + $this->dd->formatOutput = true; + $this->dd->loadXML($xml->asXML()); + + $this->eCategories = $this->dd + ->getElementsByTagName('categories')->item(0); + $this->eOffers = $this->dd + ->getElementsByTagName('offers')->item(0); + + $this->addCategories($categories); + $this->addOffers($offers); + + $this->dd->saveXML(); + $this->dd->save($this->file); + } + + private function addCategories($categories) + { + foreach ($categories as $category) { + $e = $this->eCategories->appendChild( + $this->dd->createElement( + 'category', $category['name'] + ) + ); + + $e->setAttribute('id', $category['id']); + + if ($category['parentId'] > 0) { + $e->setAttribute('parentId', $category['parentId']); + } + } + } + + private function addOffers($offers) + { + foreach ($offers as $offer) { + + $e = $this->eOffers->appendChild( + $this->dd->createElement('offer') + ); + + $e->setAttribute('id', $offer['id']); + $e->setAttribute('productId', $offer['productId']); + + if (!empty($offer['quantity'])) { + $e->setAttribute('quantity', (int) $offer['quantity']); + } else { + $e->setAttribute('quantity', 0); + } + + foreach ($offer['categoryId'] as $categoryId) { + $e->appendChild( + $this->dd->createElement('categoryId', $categoryId) + ); + } + + $offerKeys = array_keys($offer); + + foreach ($offerKeys as $key) { + if (in_array($key, $this->properties)) { + $e->appendChild( + $this->dd->createElement($key) + )->appendChild( + $this->dd->createTextNode($offer[$key]) + ); + } + + if (in_array($key, array_keys($this->params))) { + $param = $this->dd->createElement('param'); + $param->setAttribute('code', $key); + $param->setAttribute('name', $this->params[$key]); + $param->appendChild( + $this->dd->createTextNode($offer[$key]) + ); + $e->appendChild($param); + } + } + } + } + +} diff --git a/retailcrm/lib/RetailcrmProxy.php b/retailcrm/lib/RetailcrmProxy.php new file mode 100644 index 0000000..7cac9ec --- /dev/null +++ b/retailcrm/lib/RetailcrmProxy.php @@ -0,0 +1,44 @@ +api = new RetailcrmApiClient($url, $key); + $this->log = $log; + } + + public function __call($method, $arguments) + { + try { + $response = call_user_func_array(array($this->api, $method), $arguments); + + if (!$response->isSuccessful()) { + $response = false; + error_log("[$method] " . $response->getErrorMsg() . "\n", 3, $this->log); + } + + if (isset($response['errors'])) { + $error = implode("\n", $response['errors']); + error_log($error . "\n", 3, $this->log); + } + + return $response; + } catch (CurlException $e) { + error_log("[$method] " . $e->getMessage() . "\n", 3, $this->log); + return false; + } catch (InvalidJsonException $e) { + error_log("[$method] " . $e->getMessage() . "\n", 3, $this->log); + return false; + } + } + +} diff --git a/retailcrm/lib/RetailcrmReferences.php b/retailcrm/lib/RetailcrmReferences.php new file mode 100644 index 0000000..1b62c1c --- /dev/null +++ b/retailcrm/lib/RetailcrmReferences.php @@ -0,0 +1,190 @@ +api = $client; + $this->default_lang = (int) Configuration::get('PS_LANG_DEFAULT'); + $this->default_currency = (int) Configuration::get('PS_CURRENCY_DEFAULT'); + $this->default_country = (int) Configuration::get('PS_COUNTRY_DEFAULT'); + } + + public function getDeliveryTypes() + { + $deliveryTypes = array(); + + $carriers = Carrier::getCarriers($this->default_lang, true, false, false, null, PS_CARRIERS_AND_CARRIER_MODULES_NEED_RANGE); + + if (!empty($carriers)) { + foreach ($carriers as $carrier) { + $deliveryTypes[] = array( + 'type' => 'select', + 'label' => $carrier['name'], + 'name' => 'RETAILCRM_API_DELIVERY[' . $carrier['id_carrier'] . ']', + 'required' => false, + 'options' => array( + 'query' => $this->getApiDeliveryTypes(), + 'id' => 'id_option', + 'name' => 'name' + ) + ); + } + } + + return $deliveryTypes; + } + + public function getStatuses() + { + $statusTypes = array(); + $states = array_merge( + OrderState::getOrderStates($this->default_lang, true), + OrderReturnState::getOrderReturnStates($this->default_lang, true) + ); + + if (!empty($states)) { + foreach ($states as $state) { + if ($state['name'] != ' ') { + $statusTypes[] = array( + 'type' => 'select', + 'label' => $state['name'], + 'name' => 'RETAILCRM_API_STATUS[' . $state['id_order_state'] . ']', + 'required' => false, + 'options' => array( + 'query' => $this->getApiStatuses(), + 'id' => 'id_option', + 'name' => 'name' + ) + ); + } + } + } + + return $statusTypes; + } + + public function getPaymentTypes() + { + $payments = $this->getSystemPaymentModules(); + $paymentTypes = array(); + + if (!empty($payments)) { + foreach ($payments as $payment) { + $paymentTypes[] = array( + 'type' => 'select', + 'label' => $payment['name'], + 'name' => 'RETAILCRM_API_PAYMENT[' . $payment['code'] . ']', + 'required' => false, + 'options' => array( + 'query' => $this->getApiPaymentTypes(), + 'id' => 'id_option', + 'name' => 'name' + ) + ); + } + } + + return $paymentTypes; + } + + protected function getSystemPaymentModules() + { + $shop_id = Context::getContext()->shop->id; + + /* Get all modules then select only payment ones */ + $modules = Module::getModulesOnDisk(true); + + foreach ($modules as $module) { + if ($module->tab == 'payments_gateways') { + if ($module->id) { + if (!get_class($module) == 'SimpleXMLElement') + $module->country = array(); + $countries = DB::getInstance()->executeS('SELECT id_country FROM ' . _DB_PREFIX_ . 'module_country WHERE id_module = ' . (int) $module->id . ' AND `id_shop`=' . (int) $shop_id); + foreach ($countries as $country) + $module->country[] = $country['id_country']; + if (!get_class($module) == 'SimpleXMLElement') + $module->currency = array(); + $currencies = DB::getInstance()->executeS('SELECT id_currency FROM ' . _DB_PREFIX_ . 'module_currency WHERE id_module = ' . (int) $module->id . ' AND `id_shop`=' . (int) $shop_id); + foreach ($currencies as $currency) + $module->currency[] = $currency['id_currency']; + if (!get_class($module) == 'SimpleXMLElement') + $module->group = array(); + $groups = DB::getInstance()->executeS('SELECT id_group FROM ' . _DB_PREFIX_ . 'module_group WHERE id_module = ' . (int) $module->id . ' AND `id_shop`=' . (int) $shop_id); + foreach ($groups as $group) + $module->group[] = $group['id_group']; + } else { + $module->country = null; + $module->currency = null; + $module->group = null; + } + + if ($module->active != 0) { + $this->payment_modules[] = array( + 'id' => $module->id, + 'code' => $module->name, + 'name' => $module->displayName + ); + } + } + } + + return $this->payment_modules; + } + + protected function getApiDeliveryTypes() + { + $crmDeliveryTypes = array(); + $request = $this->api->deliveryTypesList(); + + if ($request->isSuccessful()) { + $crmDeliveryTypes[] = array(); + foreach ($request->deliveryTypes as $dType) { + $crmDeliveryTypes[] = array( + 'id_option' => $dType['code'], + 'name' => $dType['name'], + ); + } + } + + return $crmDeliveryTypes; + } + + protected function getApiStatuses() + { + $crmStatusTypes = array(); + $request = $this->api->statusesList(); + + if ($request->isSuccessful()) { + $crmStatusTypes[] = array(); + foreach ($request->statuses as $sType) { + $crmStatusTypes[] = array( + 'id_option' => $sType['code'], + 'name' => $sType['name'] + ); + } + } + + return $crmStatusTypes; + } + + protected function getApiPaymentTypes() + { + $crmPaymentTypes = array(); + $request = $this->api->paymentTypesList(); + + if ($request->isSuccessful()) { + $crmPaymentTypes[] = array(); + foreach ($request->paymentTypes as $pType) { + $crmPaymentTypes[] = array( + 'id_option' => $pType['code'], + 'name' => $pType['name'] + ); + } + } + + return $crmPaymentTypes; + } + +} diff --git a/retailcrm/lib/RetailcrmService.php b/retailcrm/lib/RetailcrmService.php new file mode 100644 index 0000000..9a94694 --- /dev/null +++ b/retailcrm/lib/RetailcrmService.php @@ -0,0 +1,42 @@ +name = 'retailcrm'; - $this->tab = 'market_place'; - $this->version = '1.1'; - $this->author = 'Retail Driver LCC'; - - $this->displayName = $this->l('RetailCRM'); - $this->description = $this->l('Integration module for RetailCRM'); - $this->confirmUninstall = $this->l('Are you sure you want to uninstall?'); - - $this->apiUrl = Configuration::get('RETAILCRM_ADDRESS'); - $this->apiKey = Configuration::get('RETAILCRM_API_TOKEN'); - - if (!empty($this->apiUrl) && !empty($this->apiKey)) { - $this->api = new ApiClient( - $this->apiUrl, - $this->apiKey - ); - } - - $this->default_lang = (int) Configuration::get('PS_LANG_DEFAULT'); - $this->default_currency = (int) Configuration::get('PS_CURRENCY_DEFAULT'); - $this->default_country = (int) Configuration::get('PS_COUNTRY_DEFAULT'); - - $this->response = array(); - $this->customerFix = array(); - $this->orderFix = array(); - - $this->address_id = null; - $this->customer_id = null; - - $this->customer = null; - - $this->ref = new References($this->api); - - parent::__construct(); - } - - function install() - { - return ( - parent::install() && - $this->registerHook('newOrder') && - $this->registerHook('actionOrderStatusPostUpdate') && - $this->registerHook('actionPaymentConfirmation') && - $this->registerHook('actionCustomerAccountAdd') - ); - } - - function uninstall() - { - return parent::uninstall() && - Configuration::deleteByName('RETAILCRM_ADDRESS') && - Configuration::deleteByName('RETAILCRM_API_TOKEN') && - Configuration::deleteByName('RETAILCRM_API_STATUS') && - Configuration::deleteByName('RETAILCRM_API_DELIVERY') && - Configuration::deleteByName('RETAILCRM_LAST_SYNC') && - Configuration::deleteByName('RETAILCRM_API_ADDR') - ; - } - - public function getContent() - { - $output = null; - - $address = Configuration::get('RETAILCRM_ADDRESS'); - $token = Configuration::get('RETAILCRM_API_TOKEN'); - - if (!$address || $address == '') { - $output .= $this->displayError( $this->l('Invalid or empty crm address') ); - } elseif (!$token || $token == '') { - $output .= $this->displayError( $this->l('Invalid or empty crm api token') ); - } else { - $output .= $this->displayConfirmation( - $this->l('Timezone settings must be identical to both of your crm and shop') . - " $address/admin/settings#t-main" - ); - } - - if (Tools::isSubmit('submit'.$this->name)) - { - $address = strval(Tools::getValue('RETAILCRM_ADDRESS')); - $token = strval(Tools::getValue('RETAILCRM_API_TOKEN')); - $delivery = json_encode(Tools::getValue('RETAILCRM_API_DELIVERY')); - $status = json_encode(Tools::getValue('RETAILCRM_API_STATUS')); - $payment = json_encode(Tools::getValue('RETAILCRM_API_PAYMENT')); - $order_address = json_encode(Tools::getValue('RETAILCRM_API_ADDR')); - - if (!$address || empty($address) || !Validate::isGenericName($address)) { - $output .= $this->displayError( $this->l('Invalid crm address') ); - } elseif (!$token || empty($token) || !Validate::isGenericName($token)) { - $output .= $this->displayError( $this->l('Invalid crm api token') ); - } else { - Configuration::updateValue('RETAILCRM_ADDRESS', $address); - Configuration::updateValue('RETAILCRM_API_TOKEN', $token); - Configuration::updateValue('RETAILCRM_API_DELIVERY', $delivery); - Configuration::updateValue('RETAILCRM_API_STATUS', $status); - Configuration::updateValue('RETAILCRM_API_PAYMENT', $payment); - Configuration::updateValue('RETAILCRM_API_ADDR', $order_address); - $output .= $this->displayConfirmation($this->l('Settings updated')); - } - } - - $this->display(__FILE__, 'retailcrm.tpl'); - - return $output.$this->displayForm(); - } - - public function displayForm() - { - - $this->displayConfirmation($this->l('Settings updated')); - - $default_lang = $this->default_lang; - $intaroCrm = $this->api; - - /* - * Network connection form - */ - $fields_form[0]['form'] = array( - 'legend' => array( - 'title' => $this->l('Network connection'), - ), - 'input' => array( - array( - 'type' => 'text', - 'label' => $this->l('CRM address'), - 'name' => 'RETAILCRM_ADDRESS', - 'size' => 20, - 'required' => true - ), - array( - 'type' => 'text', - 'label' => $this->l('CRM token'), - 'name' => 'RETAILCRM_API_TOKEN', - 'size' => 20, - 'required' => true - ) - ), - 'submit' => array( - 'title' => $this->l('Save'), - 'class' => 'button' - ) - ); - - - if (!empty($this->apiUrl) && !empty($this->apiKey)) { - /* - * Delivery - */ - $fields_form[1]['form'] = array( - 'legend' => array( - 'title' => $this->l('Delivery'), - ), - 'input' => $this->ref->getDeliveryTypes(), - ); - - /* - * Order status - */ - $fields_form[2]['form'] = array( - 'legend' => array( - 'title' => $this->l('Order statuses'), - ), - 'input' => $this->ref->getStatuses(), - ); - - /* - * Payment - */ - $fields_form[3]['form'] = array( - 'legend' => array( - 'title' => $this->l('Payment types'), - ), - 'input' => $this->ref->getPaymentTypes(), - ); - } - - /* - * Address fields - */ - $fields_form[4]['form'] = array( - 'legend' => array( - 'title' => $this->l('Address'), - ), - 'input' => $this->getAddressFields() - ); - - - /* - * Diplay forms - */ - - $helper = new HelperForm(); - - $helper->module = $this; - $helper->name_controller = $this->name; - $helper->token = Tools::getAdminTokenLite('AdminModules'); - $helper->currentIndex = AdminController::$currentIndex.'&configure='.$this->name; - - $helper->default_form_language = $default_lang; - $helper->allow_employee_form_lang = $default_lang; - - $helper->title = $this->displayName; - $helper->show_toolbar = true; - $helper->toolbar_scroll = true; - $helper->submit_action = 'submit'.$this->name; - $helper->toolbar_btn = array( - 'save' => - array( - 'desc' => $this->l('Save'), - 'href' => AdminController::$currentIndex.'&configure='.$this->name.'&save'.$this->name. - '&token='.Tools::getAdminTokenLite('AdminModules'), - ), - 'back' => array( - 'href' => AdminController::$currentIndex.'&token='.Tools::getAdminTokenLite('AdminModules'), - 'desc' => $this->l('Back to list') - ) - ); - - $helper->fields_value['RETAILCRM_ADDRESS'] = Configuration::get('RETAILCRM_ADDRESS'); - $helper->fields_value['RETAILCRM_API_TOKEN'] = Configuration::get('RETAILCRM_API_TOKEN'); - - $deliverySettings = Configuration::get('RETAILCRM_API_DELIVERY'); - if (isset($deliverySettings) && $deliverySettings != '') - { - $deliveryTypes = json_decode($deliverySettings); - foreach ($deliveryTypes as $idx => $delivery) { - $name = 'RETAILCRM_API_DELIVERY[' . $idx . ']'; - $helper->fields_value[$name] = $delivery; - } - } - - $statusSettings = Configuration::get('RETAILCRM_API_STATUS'); - if (isset($statusSettings) && $statusSettings != '') - { - $statusTypes = json_decode($statusSettings); - foreach ($statusTypes as $idx => $status) { - $name = 'RETAILCRM_API_STATUS[' . $idx . ']'; - $helper->fields_value[$name] = $status; - } - } - - $paymentSettings = Configuration::get('RETAILCRM_API_PAYMENT'); - if (isset($paymentSettings) && $paymentSettings != '') - { - $paymentTypes = json_decode($paymentSettings); - foreach ($paymentTypes as $idx => $payment) { - $name = 'RETAILCRM_API_PAYMENT[' . $idx . ']'; - $helper->fields_value[$name] = $payment; - } - } - - $addressSettings = Configuration::get('RETAILCRM_API_ADDR'); - if (isset($addressSettings) && $addressSettings != '') - { - $addressTypes = json_decode($addressSettings); - foreach ($addressTypes as $idx => $address) { - $name = 'RETAILCRM_API_ADDR[' . $idx . ']'; - $helper->fields_value[$name] = $address; - } - } - - return $helper->generateForm($fields_form); - } - - public function getAddressFields() - { - $addressFields = array(); - $address = explode(' ', str_replace("\n", ' ', AddressFormat::getAddressCountryFormat($this->context->country->id))); - - if (!empty($address)) { - foreach ($address as $idx => $a) { - if (!in_array($a, array('vat_number', 'phone_mobile', 'company'))) { - if (!strpos($a, ':')) { - $a = preg_replace('/_/', ' ', $a); - $a = preg_replace('/[\,\.]/', '', $a); - $addressFields[] = array( - 'type' => 'select', - 'label' => $this->l((string) ucfirst($a)), - 'name' => 'RETAILCRM_API_ADDR[' . $idx . ']', - 'required' => false, - 'options' => array( - 'query' => array( - array( - 'name' => '', - 'id_option' => '' - ), - array( - 'name' => $this->l('FIRST_NAME'), - 'id_option' => 'first_name' - ), - array( - 'name' => $this->l('LAST_NAME'), - 'id_option' => 'last_name' - ), - array( - 'name' => $this->l('PHONE'), - 'id_option' => 'phone' - ), - array( - 'name' => $this->l('EMAIL'), - 'id_option' => 'email' - ), - array( - 'name' => $this->l('ADDRESS'), - 'id_option' => 'address' - ), - array( - 'name' => $this->l('COUNTRY'), - 'id_option' => 'country' - ), - array( - 'name' => $this->l('REGION'), - 'id_option' => 'region' - ), - array( - 'name' => $this->l('CITY'), - 'id_option' => 'city' - ), - array( - 'name' => $this->l('ZIP'), - 'id_option' => 'index' - ), - array( - 'name' => $this->l('STREET'), - 'id_option' => 'street' - ), - array( - 'name' => $this->l('BUILDING'), - 'id_option' => 'building' - ), - array( - 'name' => $this->l('FLAT'), - 'id_option' => 'flat' - ), - array( - 'name' => $this->l('INTERCOMCODE'), - 'id_option' => 'intercomcode' - ), - array( - 'name' => $this->l('FLOOR'), - 'id_option' => 'floor' - ), - array( - 'name' => $this->l('BLOCK'), - 'id_option' => 'block' - ), - array( - 'name' => $this->l('HOUSE'), - 'ID' => 'house' - ) - ), - 'id' => 'id_option', - 'name' => 'name' - ) - ); - } - } - } - } - - return $addressFields; - } - - public function hookActionCustomerAccountAdd($params) - { - try { - $this->api->customersCreate( - array( - 'externalId' => $params['newCustomer']->id, - 'firstName' => $params['newCustomer']->firstname, - 'lastName' => $params['newCustomer']->lastname, - 'email' => $params['newCustomer']->email, - 'createdAt' => $params['newCustomer']->date_add - ) - ); - } - catch (CurlException $e) { - error_log('customerCreate: connection error', 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); - } - catch (InvalidJsonException $e) { - error_log('customerCreate: ' . $e->getMessage(), 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); - } - } - - public function hookNewOrder($params) - { - return $this->hookActionOrderStatusPostUpdate($params); - } - - public function hookActionPaymentConfirmation($params) - { - $this->api->ordersEdit( - array( - 'externalId' => $params['id_order'], - 'paymentStatus' => 'paid', - 'createdAt' => $params['cart']->date_upd - ) - ); - - return $this->hookActionOrderStatusPostUpdate($params); - } - - public function hookActionOrderStatusPostUpdate($params) - { - $address_id = Address::getFirstCustomerAddressId($params['cart']->id_customer); - $sql = 'SELECT * FROM '._DB_PREFIX_.'address WHERE id_address='.(int) $address_id; - $address = Db::getInstance()->ExecuteS($sql); - $address = $address[0]; - $delivery = json_decode(Configuration::get('RETAILCRM_API_DELIVERY')); - $payment = json_decode(Configuration::get('RETAILCRM_API_PAYMENT')); - $inCart = $params['cart']->getProducts(); - - if (isset($params['orderStatus'])) { - try { - $this->api->customersCreate( - array( - 'externalId' => $params['cart']->id_customer, - 'lastName' => $params['customer']->lastname, - 'firstName' => $params['customer']->firstname, - 'email' => $params['customer']->email, - 'phones' => array( - array( - 'number' => $address['phone'], - 'type' => 'mobile' - ), - array( - 'number' => $address['phone_mobile'], - 'type' => 'mobile' - ) - ), - 'createdAt' => $params['customer']->date_add - ) - ); - } - catch (CurlException $e) { - error_log("customerCreate: connection error", 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); - } - catch (InvalidJsonException $e) { - error_log('customerCreate: ' . $e->getMessage(), 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); - } - - try { - $items = array(); - foreach ($inCart as $item) { - $items[] = array( - 'initialPrice' => (!empty($item['rate'])) ? $item['price'] + ($item['price'] * $item['rate'] / 100) : $item['price'], - 'quantity' => $item['quantity'], - 'productId' => $item['id_product'], - 'productName' => $item['name'], - 'createdAt' => $item['date_add'] - ); - } - - $dTypeKey = $params['cart']->id_carrier; - - if (Module::getInstanceByName('advancedcheckout') === false) { - $pTypeKey = $params['order']->module; - } else { - $pTypeKey = $params['order']->payment; - } - - $this->api->ordersCreate( - array( - 'externalId' => $params['order']->id, - 'orderType' => 'eshop-individual', - 'orderMethod' => 'shopping-cart', - 'status' => 'new', - 'customerId' => $params['cart']->id_customer, - 'firstName' => $params['customer']->firstname, - 'lastName' => $params['customer']->lastname, - 'phone' => $address['phone'], - 'email' => $params['customer']->email, - 'paymentType' => $payment->$pTypeKey, - 'delivery' => array( - 'code' => $delivery->$dTypeKey, - 'cost' => $params['order']->total_shipping, - 'address' => array( - 'city' => $address['city'], - 'index' => $address['postcode'], - 'text' => $address['address1'], - ) - ), - 'discount' => $params['order']->total_discounts, - 'items' => $items, - 'createdAt' => $params['order']->date_add - ) - ); - } - catch (CurlException $e) { - error_log('orderCreate: connection error', 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); - } - catch (InvalidJsonException $e) { - error_log('orderCreate: ' . $e->getMessage(), 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); - } - - } - - if (!empty($params['newOrderStatus'])) { - $statuses = OrderState::getOrderStates($this->default_lang); - $aStatuses = json_decode(Configuration::get('RETAILCRM_API_STATUS')); - foreach ($statuses as $status) { - if ($status['name'] == $params['newOrderStatus']->name) { - $currStatus = $status['id_order_state']; - try { - $this->api->ordersEdit( - array( - 'externalId' => $params['id_order'], - 'status' => $aStatuses->$currStatus - ) - ); - } - catch (CurlException $e) { - error_log('orderStatusUpdate: connection error', 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); - } - catch (InvalidJsonException $e) { - error_log('orderStatusUpdate: ' . $e->getMessage(), 3, _PS_ROOT_DIR_ . "log/retailcrm.log"); - } - } - } - } + $allowed = array('1.4', '1.5', '1.6'); + $version = substr(_PS_VERSION_, 0, 3); + if (!in_array($version, $allowed)) { + exit; + } else { + require_once (dirname(__FILE__) . '/bootstrap.php'); + require(dirname(__FILE__) . '/version.' . $version . '.php'); } } + diff --git a/retailcrm/version.1.6.php b/retailcrm/version.1.6.php new file mode 100644 index 0000000..188ac27 --- /dev/null +++ b/retailcrm/version.1.6.php @@ -0,0 +1,433 @@ +name = 'retailcrm'; + $this->tab = 'market_place'; + $this->version = '1.1'; + $this->author = 'Retail Driver LCC'; + $this->displayName = $this->l('RetailCRM'); + $this->description = $this->l('Integration module for RetailCRM'); + $this->confirmUninstall = $this->l('Are you sure you want to uninstall?'); + + $this->apiUrl = Configuration::get('RETAILCRM_ADDRESS'); + $this->apiKey = Configuration::get('RETAILCRM_API_TOKEN'); + + if (!empty($this->apiUrl) && !empty($this->apiKey)) { + $this->api = new RetailcrmProxy($this->apiUrl, $this->apiKey, _PS_ROOT_DIR_ . '/retailcrm.log'); + $this->reference = new RetailcrmReferences($this->api); + } + $this->default_lang = (int)Configuration::get('PS_LANG_DEFAULT'); + $this->default_currency = (int)Configuration::get('PS_CURRENCY_DEFAULT'); + $this->default_country = (int)Configuration::get('PS_COUNTRY_DEFAULT'); + $this->response = array(); + $this->customerFix = array(); + $this->orderFix = array(); + $this->address_id = null; + $this->customer_id = null; + $this->customer = null; + parent::__construct(); + } + + function install() + { + return ( + parent::install() && + $this->registerHook('newOrder') && + $this->registerHook('actionOrderStatusPostUpdate') && + $this->registerHook('actionPaymentConfirmation') && + $this->registerHook('actionCustomerAccountAdd') + ); + } + + function uninstall() + { + return parent::uninstall() && + Configuration::deleteByName('RETAILCRM_ADDRESS') && + Configuration::deleteByName('RETAILCRM_API_TOKEN') && + Configuration::deleteByName('RETAILCRM_API_STATUS') && + Configuration::deleteByName('RETAILCRM_API_DELIVERY') && + Configuration::deleteByName('RETAILCRM_LAST_SYNC') && + Configuration::deleteByName('RETAILCRM_API_ADDR'); + } + + public function getContent() + { + $output = null; + + $address = Configuration::get('RETAILCRM_ADDRESS'); + $token = Configuration::get('RETAILCRM_API_TOKEN'); + + if (!$address || $address == '') { + $output .= $this->displayError($this->l('Invalid or empty crm address')); + } elseif (!$token || $token == '') { + $output .= $this->displayError($this->l('Invalid or empty crm api token')); + } else { + $output .= $this->displayConfirmation( + $this->l('Timezone settings must be identical to both of your crm and shop') . + " $address/admin/settings#t-main" + ); + } + + if (Tools::isSubmit('submit' . $this->name)) { + $address = strval(Tools::getValue('RETAILCRM_ADDRESS')); + $token = strval(Tools::getValue('RETAILCRM_API_TOKEN')); + $delivery = json_encode(Tools::getValue('RETAILCRM_API_DELIVERY')); + $status = json_encode(Tools::getValue('RETAILCRM_API_STATUS')); + $payment = json_encode(Tools::getValue('RETAILCRM_API_PAYMENT')); + $order_address = json_encode(Tools::getValue('RETAILCRM_API_ADDR')); + + if (!$address || empty($address) || !Validate::isGenericName($address)) { + $output .= $this->displayError($this->l('Invalid crm address')); + } elseif (!$token || empty($token) || !Validate::isGenericName($token)) { + $output .= $this->displayError($this->l('Invalid crm api token')); + } else { + Configuration::updateValue('RETAILCRM_ADDRESS', $address); + Configuration::updateValue('RETAILCRM_API_TOKEN', $token); + Configuration::updateValue('RETAILCRM_API_DELIVERY', $delivery); + Configuration::updateValue('RETAILCRM_API_STATUS', $status); + Configuration::updateValue('RETAILCRM_API_PAYMENT', $payment); + Configuration::updateValue('RETAILCRM_API_ADDR', $order_address); + $output .= $this->displayConfirmation($this->l('Settings updated')); + } + } + + $this->display(__FILE__, 'retailcrm.tpl'); + + return $output . $this->displayForm(); + } + + public function displayForm() + { + + $this->displayConfirmation($this->l('Settings updated')); + + $default_lang = $this->default_lang; + $intaroCrm = $this->api; + + /* + * Network connection form + */ + $fields_form[0]['form'] = array( + 'legend' => array( + 'title' => $this->l('Network connection'), + ), + 'input' => array( + array( + 'type' => 'text', + 'label' => $this->l('CRM address'), + 'name' => 'RETAILCRM_ADDRESS', + 'size' => 20, + 'required' => true + ), + array( + 'type' => 'text', + 'label' => $this->l('CRM token'), + 'name' => 'RETAILCRM_API_TOKEN', + 'size' => 20, + 'required' => true + ) + ), + 'submit' => array( + 'title' => $this->l('Save'), + 'class' => 'button' + ) + ); + + + if (!empty($this->apiUrl) && !empty($this->apiKey)) { + /* + * Delivery + */ + $fields_form[1]['form'] = array( + 'legend' => array( + 'title' => $this->l('Delivery'), + ), + 'input' => $this->reference->getDeliveryTypes(), + ); + + /* + * Order status + */ + $fields_form[2]['form'] = array( + 'legend' => array( + 'title' => $this->l('Order statuses'), + ), + 'input' => $this->reference->getStatuses(), + ); + + /* + * Payment + */ + $fields_form[3]['form'] = array( + 'legend' => array( + 'title' => $this->l('Payment types'), + ), + 'input' => $this->reference->getPaymentTypes(), + ); + } + + /* + * Address fields + */ + $fields_form[4]['form'] = array( + 'legend' => array( + 'title' => $this->l('Address'), + ), + 'input' => $this->getAddressFields() + ); + + + /* + * Diplay forms + */ + + $helper = new HelperForm(); + + $helper->module = $this; + $helper->name_controller = $this->name; + $helper->token = Tools::getAdminTokenLite('AdminModules'); + $helper->currentIndex = AdminController::$currentIndex . '&configure=' . $this->name; + + $helper->default_form_language = $default_lang; + $helper->allow_employee_form_lang = $default_lang; + + $helper->title = $this->displayName; + $helper->show_toolbar = true; + $helper->toolbar_scroll = true; + $helper->submit_action = 'submit' . $this->name; + $helper->toolbar_btn = array( + 'save' => + array( + 'desc' => $this->l('Save'), + 'href' => sprintf( + "%s&configure=%s&save%s&token=%s", + AdminController::$currentIndex, + $this->name, + $this->name, + Tools::getAdminTokenLite('AdminModules') + ) + ), + 'back' => array( + 'href' => AdminController::$currentIndex . '&token=' . Tools::getAdminTokenLite('AdminModules'), + 'desc' => $this->l('Back to list') + ) + ); + + $helper->fields_value['RETAILCRM_ADDRESS'] = Configuration::get('RETAILCRM_ADDRESS'); + $helper->fields_value['RETAILCRM_API_TOKEN'] = Configuration::get('RETAILCRM_API_TOKEN'); + + $deliverySettings = Configuration::get('RETAILCRM_API_DELIVERY'); + if (isset($deliverySettings) && $deliverySettings != '') { + $deliveryTypes = json_decode($deliverySettings); + foreach ($deliveryTypes as $idx => $delivery) { + $name = 'RETAILCRM_API_DELIVERY[' . $idx . ']'; + $helper->fields_value[$name] = $delivery; + } + } + + $statusSettings = Configuration::get('RETAILCRM_API_STATUS'); + if (isset($statusSettings) && $statusSettings != '') { + $statusTypes = json_decode($statusSettings); + foreach ($statusTypes as $idx => $status) { + $name = 'RETAILCRM_API_STATUS[' . $idx . ']'; + $helper->fields_value[$name] = $status; + } + } + + $paymentSettings = Configuration::get('RETAILCRM_API_PAYMENT'); + if (isset($paymentSettings) && $paymentSettings != '') { + $paymentTypes = json_decode($paymentSettings); + foreach ($paymentTypes as $idx => $payment) { + $name = 'RETAILCRM_API_PAYMENT[' . $idx . ']'; + $helper->fields_value[$name] = $payment; + } + } + + $addressSettings = Configuration::get('RETAILCRM_API_ADDR'); + if (isset($addressSettings) && $addressSettings != '') { + $addressTypes = json_decode($addressSettings); + foreach ($addressTypes as $idx => $address) { + $name = 'RETAILCRM_API_ADDR[' . $idx . ']'; + $helper->fields_value[$name] = $address; + } + } + + return $helper->generateForm($fields_form); + } + + public function getAddressFields() + { + $addressFields = array(); + $address = explode(' ', str_replace("\n", ' ', AddressFormat::getAddressCountryFormat($this->context->country->id))); + + if (!empty($address)) { + foreach ($address as $idx => $a) { + if (!in_array($a, array('vat_number', 'phone_mobile', 'company'))) { + if (!strpos($a, ':')) { + $a = preg_replace('/_/', ' ', $a); + $a = preg_replace('/[\,\.]/', '', $a); + $addressFields[] = array( + 'type' => 'select', + 'label' => $this->l((string)ucfirst($a)), + 'name' => 'RETAILCRM_API_ADDR[' . $idx . ']', + 'required' => false, + 'options' => array( + 'query' => array( + array('name' => '', 'id_option' => ''), + array('name' => $this->l('FIRST_NAME'), 'id_option' => 'first_name'), + array('name' => $this->l('LAST_NAME'), 'id_option' => 'last_name'), + array('name' => $this->l('PHONE'), 'id_option' => 'phone'), + array('name' => $this->l('EMAIL'), 'id_option' => 'email'), + array('name' => $this->l('ADDRESS'), 'id_option' => 'address'), + array('name' => $this->l('COUNTRY'), 'id_option' => 'country'), + array('name' => $this->l('REGION'), 'id_option' => 'region'), + array('name' => $this->l('CITY'), 'id_option' => 'city'), + array('name' => $this->l('ZIP'), 'id_option' => 'index'), + array('name' => $this->l('STREET'), 'id_option' => 'street'), + array('name' => $this->l('BUILDING'), 'id_option' => 'building'), + array('name' => $this->l('FLAT'), 'id_option' => 'flat'), + array('name' => $this->l('INTERCOMCODE'), 'id_option' => 'intercomcode'), + array('name' => $this->l('FLOOR'), 'id_option' => 'floor'), + array('name' => $this->l('BLOCK'), 'id_option' => 'block'), + array('name' => $this->l('HOUSE'), 'ID' => 'house') + ), + 'id' => 'id_option', + 'name' => 'name' + ) + ); + } + } + } + } + + return $addressFields; + } + + public function hookActionCustomerAccountAdd($params) + { + $this->api->customersCreate( + array( + 'externalId' => $params['newCustomer']->id, + 'firstName' => $params['newCustomer']->firstname, + 'lastName' => $params['newCustomer']->lastname, + 'email' => $params['newCustomer']->email, + 'createdAt' => $params['newCustomer']->date_add + ) + ); + } + + public function hookNewOrder($params) + { + return $this->hookActionOrderStatusPostUpdate($params); + } + + public function hookActionPaymentConfirmation($params) + { + $this->api->ordersEdit( + array( + 'externalId' => $params['id_order'], + 'paymentStatus' => 'paid', + 'createdAt' => $params['cart']->date_upd + ) + ); + + return $this->hookActionOrderStatusPostUpdate($params); + } + + public function hookActionOrderStatusPostUpdate($params) + { + $address_id = Address::getFirstCustomerAddressId($params['cart']->id_customer); + $sql = 'SELECT * FROM ' . _DB_PREFIX_ . 'address WHERE id_address=' . (int)$address_id; + $dbaddress = Db::getInstance()->ExecuteS($sql); + $address = $dbaddress[0]; + $delivery = json_decode(Configuration::get('RETAILCRM_API_DELIVERY')); + $payment = json_decode(Configuration::get('RETAILCRM_API_PAYMENT')); + $inCart = $params['cart']->getProducts(); + + if (isset($params['orderStatus'])) { + $this->api->customersEdit( + array( + 'externalId' => $params['cart']->id_customer, + 'lastName' => $params['customer']->lastname, + 'firstName' => $params['customer']->firstname, + 'email' => $params['customer']->email, + 'phones' => array( + array( + 'number' => $address['phone'], + ), + array( + 'number' => $address['phone_mobile'], + ) + ), + 'createdAt' => $params['customer']->date_add + ) + ); + + $items = array(); + foreach ($inCart as $item) { + $items[] = array( + 'initialPrice' => (!empty($item['rate'])) ? $item['price'] + ($item['price'] * $item['rate'] / 100) : $item['price'], + 'quantity' => $item['quantity'], + 'productId' => $item['id_product'], + 'productName' => $item['name'], + 'createdAt' => $item['date_add'] + ); + } + + $dTypeKey = $params['cart']->id_carrier; + + if (Module::getInstanceByName('advancedcheckout') === false) { + $pTypeKey = $params['order']->module; + } else { + $pTypeKey = $params['order']->payment; + } + + $this->api->ordersCreate( + array( + 'externalId' => $params['order']->id, + 'orderType' => 'eshop-individual', + 'orderMethod' => 'shopping-cart', + 'status' => 'new', + 'customerId' => $params['cart']->id_customer, + 'firstName' => $params['customer']->firstname, + 'lastName' => $params['customer']->lastname, + 'phone' => $address['phone'], + 'email' => $params['customer']->email, + 'paymentType' => $payment->$pTypeKey, + 'delivery' => array( + 'code' => $delivery->$dTypeKey, + 'cost' => $params['order']->total_shipping, + 'address' => array( + 'city' => $address['city'], + 'index' => $address['postcode'], + 'text' => $address['address1'], + ) + ), + 'discount' => $params['order']->total_discounts, + 'items' => $items, + 'createdAt' => $params['order']->date_add + ) + ); + } + + if (!empty($params['newOrderStatus'])) { + $statuses = OrderState::getOrderStates($this->default_lang); + $aStatuses = json_decode(Configuration::get('RETAILCRM_API_STATUS')); + foreach ($statuses as $status) { + if ($status['name'] == $params['newOrderStatus']->name) { + $currStatus = $status['id_order_state']; + $this->api->ordersEdit( + array( + 'externalId' => $params['id_order'], + 'status' => $aStatuses->$currStatus + ) + ); + } + } + } + } +} From fc0b1d33f99246068cb462bcdbf72e65ebe0a2c1 Mon Sep 17 00:00:00 2001 From: Alex Lushpai Date: Tue, 12 Jan 2016 23:06:05 +0300 Subject: [PATCH 14/17] version dependency, interface update, bug fixes --- retailcrm/config.xml | 6 +- retailcrm/config_ru.xml | 6 +- retailcrm/lib/RetailcrmProxy.php | 11 +- retailcrm/lib/RetailcrmReferences.php | 38 +- retailcrm/lib/classes/Catalog.php | 137 ---- retailcrm/lib/classes/References.php | 210 ----- retailcrm/lib/custom/.gitkeep | 0 retailcrm/lib/vendor/Icml.php | 137 ---- retailcrm/lib/vendor/Retailcrm.php | 1055 ------------------------- retailcrm/lib/vendor/Service.php | 43 - retailcrm/logo.png | Bin 957 -> 5071 bytes retailcrm/retailcrm.php | 439 +++++++++- retailcrm/version.1.6.php | 433 ---------- 13 files changed, 469 insertions(+), 2046 deletions(-) delete mode 100644 retailcrm/lib/classes/Catalog.php delete mode 100644 retailcrm/lib/classes/References.php delete mode 100644 retailcrm/lib/custom/.gitkeep delete mode 100644 retailcrm/lib/vendor/Icml.php delete mode 100644 retailcrm/lib/vendor/Retailcrm.php delete mode 100644 retailcrm/lib/vendor/Service.php delete mode 100644 retailcrm/version.1.6.php diff --git a/retailcrm/config.xml b/retailcrm/config.xml index 40f96b7..5252419 100644 --- a/retailcrm/config.xml +++ b/retailcrm/config.xml @@ -2,12 +2,12 @@ retailcrm - + - + 1 1 - \ No newline at end of file + diff --git a/retailcrm/config_ru.xml b/retailcrm/config_ru.xml index 46eb1b8..7a93904 100644 --- a/retailcrm/config_ru.xml +++ b/retailcrm/config_ru.xml @@ -2,12 +2,12 @@ retailcrm - + - + 1 1 - \ No newline at end of file + diff --git a/retailcrm/lib/RetailcrmProxy.php b/retailcrm/lib/RetailcrmProxy.php index 7cac9ec..f99f740 100644 --- a/retailcrm/lib/RetailcrmProxy.php +++ b/retailcrm/lib/RetailcrmProxy.php @@ -22,13 +22,12 @@ class RetailcrmProxy $response = call_user_func_array(array($this->api, $method), $arguments); if (!$response->isSuccessful()) { - $response = false; error_log("[$method] " . $response->getErrorMsg() . "\n", 3, $this->log); - } - - if (isset($response['errors'])) { - $error = implode("\n", $response['errors']); - error_log($error . "\n", 3, $this->log); + if (isset($response['errors'])) { + $error = implode("\n", $response['errors']); + error_log($error . "\n", 3, $this->log); + } + $response = false; } return $response; diff --git a/retailcrm/lib/RetailcrmReferences.php b/retailcrm/lib/RetailcrmReferences.php index 1b62c1c..0f0d87d 100644 --- a/retailcrm/lib/RetailcrmReferences.php +++ b/retailcrm/lib/RetailcrmReferences.php @@ -7,15 +7,20 @@ class RetailcrmReferences { $this->api = $client; $this->default_lang = (int) Configuration::get('PS_LANG_DEFAULT'); - $this->default_currency = (int) Configuration::get('PS_CURRENCY_DEFAULT'); - $this->default_country = (int) Configuration::get('PS_COUNTRY_DEFAULT'); } public function getDeliveryTypes() { $deliveryTypes = array(); - $carriers = Carrier::getCarriers($this->default_lang, true, false, false, null, PS_CARRIERS_AND_CARRIER_MODULES_NEED_RANGE); + $carriers = Carrier::getCarriers( + $this->default_lang, + true, + false, + false, + null, + PS_CARRIERS_AND_CARRIER_MODULES_NEED_RANGE + ); if (!empty($carriers)) { foreach ($carriers as $carrier) { @@ -47,10 +52,14 @@ class RetailcrmReferences if (!empty($states)) { foreach ($states as $state) { if ($state['name'] != ' ') { + $key = isset($state['id_order_state']) + ? $state['id_order_state'] + : $state['id_order_return_state'] + ; $statusTypes[] = array( 'type' => 'select', 'label' => $state['name'], - 'name' => 'RETAILCRM_API_STATUS[' . $state['id_order_state'] . ']', + 'name' => "RETAILCRM_API_STATUS[$key]", 'required' => false, 'options' => array( 'query' => $this->getApiStatuses(), @@ -138,8 +147,11 @@ class RetailcrmReferences $crmDeliveryTypes = array(); $request = $this->api->deliveryTypesList(); - if ($request->isSuccessful()) { - $crmDeliveryTypes[] = array(); + if ($request) { + $crmDeliveryTypes[] = array( + 'id_option' => '', + 'name' => '', + ); foreach ($request->deliveryTypes as $dType) { $crmDeliveryTypes[] = array( 'id_option' => $dType['code'], @@ -156,8 +168,11 @@ class RetailcrmReferences $crmStatusTypes = array(); $request = $this->api->statusesList(); - if ($request->isSuccessful()) { - $crmStatusTypes[] = array(); + if ($request) { + $crmStatusTypes[] = array( + 'id_option' => '', + 'name' => '', + ); foreach ($request->statuses as $sType) { $crmStatusTypes[] = array( 'id_option' => $sType['code'], @@ -174,8 +189,11 @@ class RetailcrmReferences $crmPaymentTypes = array(); $request = $this->api->paymentTypesList(); - if ($request->isSuccessful()) { - $crmPaymentTypes[] = array(); + if ($request) { + $crmPaymentTypes[] = array( + 'id_option' => '', + 'name' => '', + ); foreach ($request->paymentTypes as $pType) { $crmPaymentTypes[] = array( 'id_option' => $pType['code'], diff --git a/retailcrm/lib/classes/Catalog.php b/retailcrm/lib/classes/Catalog.php deleted file mode 100644 index 596ee8f..0000000 --- a/retailcrm/lib/classes/Catalog.php +++ /dev/null @@ -1,137 +0,0 @@ -default_lang = (int) Configuration::get('PS_LANG_DEFAULT'); - $this->default_currency = (int) Configuration::get('PS_CURRENCY_DEFAULT'); - $this->default_country = (int) Configuration::get('PS_COUNTRY_DEFAULT'); - } - - public function getData() - { - - $id_lang = (int) Configuration::get('PS_LANG_DEFAULT'); - $currency = new Currency(Configuration::get('PS_CURRENCY_DEFAULT')); - $shop_url = (Configuration::get('PS_SSL_ENABLED') ? _PS_BASE_URL_SSL_ : _PS_BASE_URL_); - - $items = array(); - $categories = array(); - - if ($currency->iso_code == 'RUB') { - $currency->iso_code = 'RUR'; - } - - // Get currencies - $currencies = Currency::getCurrencies(); - - // Get categories - $types = Category::getCategories($id_lang, true, false); - foreach ($types AS $category) - { - $categories[] = array( - 'id' => $category['id_category'], - 'parentId' => $category['id_parent'], - 'name' => $category['name'] - ); - } - - // Get products - $products = Product::getProducts($id_lang, 0, 0, 'name', 'asc'); - - foreach ($products AS $product) - { - // Check for home category - $category = $product['id_category_default']; - - if ($category == Configuration::get('PS_HOME_CATEGORY')) { - $temp_categories = Product::getProductCategories($product['id_product']); - - foreach ($temp_categories AS $category) { - if ($category != Configuration::get('PS_HOME_CATEGORY')) - break; - } - - if ($category == Configuration::get('PS_HOME_CATEGORY')) { - continue; - } - - } - - $link = new Link(); - $cover = Image::getCover($product['id_product']); - - $picture = 'http://' . $link->getImageLink($product['link_rewrite'], $product['id_product'].'-'.$cover['id_image'], 'large_default'); - if (!(substr($picture, 0, strlen($shop_url)) === $shop_url)) { - $picture = rtrim($shop_url,"/") . $picture; - } - - $crewrite = Category::getLinkRewrite($product['id_category_default'], $id_lang); - $url = $link->getProductLink($product['id_product'], $product['link_rewrite'], $crewrite); - $version = substr(_PS_VERSION_, 0, 3); - - if ($version == "1.3") - $available_for_order = $product['active'] && $product['quantity']; - else { - $prod = new Product($product['id_product']); - $available_for_order = $product['active'] && $product['available_for_order'] && $prod->checkQty(1); - } - - $item = array( - 'id' => $product['id_product'], - 'productId' => $product['id_product'], - 'productActivity' => ($available_for_order) ? 'Y' : 'N', - 'name' => htmlspecialchars(strip_tags($product['name'])), - 'productName' => htmlspecialchars(strip_tags($product['name'])), - 'categoryId' => array($category), - 'picture' => $picture, - 'url' => $url - ); - - if (!empty($product['wholesale_price'])) { - $item['purchasePrice'] = round($product['wholesale_price'], 2); - } - - $item['initialPrice'] = !empty($product['rate']) - ? round($product['price'], 2) + (round($product['price'], 2) * $product['rate'] / 100) - : round($product['price'], 2) - ; - - - if (!empty($product['manufacturer_name'])) { - $item['vendor'] = $product['manufacturer_name']; - } - - if (!empty($product['reference'])) { - $item['article'] = htmlspecialchars($product['reference']); - } - - $weight = round($product['weight'], 2); - - if (!empty($weight)) { - $item['weight'] = $weight; - } - - $width = round($product['width'], 2); - $height = round($product['height'], 2); - $depth = round($product['depth'], 2); - - if (!empty($width)) { - if (!empty($height)) { - if (!empty($depth)) { - $item['size'] = implode('x', array($width, $height, $depth)); - } else { - $item['size'] = implode('x', array($width, $height)); - } - } - } - - $items[] = $item; - - } - - return array($categories, $items); - - } -} diff --git a/retailcrm/lib/classes/References.php b/retailcrm/lib/classes/References.php deleted file mode 100644 index f9b0a41..0000000 --- a/retailcrm/lib/classes/References.php +++ /dev/null @@ -1,210 +0,0 @@ -api = $client; - $this->default_lang = (int) Configuration::get('PS_LANG_DEFAULT'); - $this->default_currency = (int) Configuration::get('PS_CURRENCY_DEFAULT'); - $this->default_country = (int) Configuration::get('PS_COUNTRY_DEFAULT'); - } - - public function getDeliveryTypes() - { - $deliveryTypes = array(); - - $carriers = Carrier::getCarriers( - $this->default_lang, true, false, false, - null, PS_CARRIERS_AND_CARRIER_MODULES_NEED_RANGE - ); - - if (!empty($carriers)) { - foreach ($carriers as $carrier) { - $deliveryTypes[] = array( - 'type' => 'select', - 'label' => $carrier['name'], - 'name' => 'RETAILCRM_API_DELIVERY[' . $carrier['id_carrier'] . ']', - 'required' => false, - 'options' => array( - 'query' => $this->getApiDeliveryTypes(), - 'id' => 'id_option', - 'name' => 'name' - ) - ); - } - } - - return $deliveryTypes; - } - - public function getStatuses() - { - $statusTypes = array(); - $states = array_merge( - OrderState::getOrderStates($this->default_lang, true), - OrderReturnState::getOrderReturnStates($this->default_lang, true) - ); - - if (!empty($states)) { - foreach ($states as $state) { - if ($state['name'] != ' ') { - $statusTypes[] = array( - 'type' => 'select', - 'label' => $state['name'], - 'name' => 'RETAILCRM_API_STATUS[' . $state['id_order_state'] . ']', - 'required' => false, - 'options' => array( - 'query' => $this->getApiStatuses(), - 'id' => 'id_option', - 'name' => 'name' - ) - ); - } - } - } - - return $statusTypes; - } - - public function getPaymentTypes() - { - $payments = $this->getSystemPaymentModules(); - $paymentTypes = array(); - - if (!empty($payments)) { - foreach ($payments as $payment) { - $paymentTypes[] = array( - 'type' => 'select', - 'label' => $payment['name'], - 'name' => 'RETAILCRM_API_PAYMENT[' . $payment['code'] . ']', - 'required' => false, - 'options' => array( - 'query' => $this->getApiPaymentTypes(), - 'id' => 'id_option', - 'name' => 'name' - ) - ); - } - } - - return $paymentTypes; - } - - protected function getSystemPaymentModules() - { - $shop_id = Context::getContext()->shop->id; - - /* Get all modules then select only payment ones */ - $modules = Module::getModulesOnDisk(true); - - foreach ($modules as $module) { - if ($module->tab == 'payments_gateways') { - if ($module->id) { - if (!get_class($module) == 'SimpleXMLElement') $module->country = array(); - $countries = DB::getInstance()->executeS('SELECT id_country FROM ' . _DB_PREFIX_ . 'module_country WHERE id_module = ' . (int) $module->id . ' AND `id_shop`=' . (int) $shop_id); - foreach ($countries as $country) $module->country[] = $country['id_country']; - if (!get_class($module) == 'SimpleXMLElement') $module->currency = array(); - $currencies = DB::getInstance()->executeS('SELECT id_currency FROM ' . _DB_PREFIX_ . 'module_currency WHERE id_module = ' . (int) $module->id . ' AND `id_shop`=' .(int) $shop_id); - foreach ($currencies as $currency) $module->currency[] = $currency['id_currency']; - if (!get_class($module) == 'SimpleXMLElement') $module->group = array(); - $groups = DB::getInstance()->executeS('SELECT id_group FROM ' . _DB_PREFIX_ . 'module_group WHERE id_module = ' . (int) $module->id . ' AND `id_shop`=' . (int) $shop_id); - foreach ($groups as $group) $module->group[] = $group['id_group']; - } else { - $module->country = null; - $module->currency = null; - $module->group = null; - } - - if ($module->active != 0) { - $this->payment_modules[] = array( - 'id' => $module->id, - 'code' => $module->name, - 'name' => $module->displayName - ); - } - } - } - - return $this->payment_modules; - } - - protected function getApiDeliveryTypes() - { - $crmDeliveryTypes = array(); - - try { - $request = $this->api->deliveryTypesList(); - if ($request->isSuccessful()) { - $crmDeliveryTypes[] = array(); - foreach ($request->deliveryTypes as $dType) { - $crmDeliveryTypes[] = array( - 'id_option' => $dType['code'], - 'name' => $dType['name'], - ); - } - } - } - catch (CurlException $e) { - error_log('deliveryTypesList: connection error', 3, _PS_ROOT_DIR . "log/retailcrm.log"); - } - catch (InvalidJsonException $e) { - error_log('deliveryTypesList: ' . $e->getMessage(), 3, _PS_ROOT_DIR . "log/retailcrm.log"); - } - - return $crmDeliveryTypes; - - } - - protected function getApiStatuses() - { - $crmStatusTypes = array(); - - try { - $request = $this->api->statusesList(); - if ($request->isSuccessful()) { - $crmStatusTypes[] = array(); - foreach ($request->statuses as $sType) { - $crmStatusTypes[] = array( - 'id_option' => $sType['code'], - 'name' => $sType['name'] - ); - } - } - } - catch (CurlException $e) { - error_log('statusTypesList: connection error', 3, _PS_ROOT_DIR . "log/retailcrm.log"); - } - catch (InvalidJsonException $e) { - error_log('statusTypesList: ' . $e->getMessage(), 3, _PS_ROOT_DIR . "log/retailcrm.log"); - } - - return $crmStatusTypes; - } - - protected function getApiPaymentTypes() - { - $crmPaymentTypes = array(); - - try { - $request = $this->api->paymentTypesList(); - if ($request->isSuccessful()) { - $crmPaymentTypes[] = array(); - foreach ($request->paymentTypes as $pType) { - $crmPaymentTypes[] = array( - 'id_option' => $pType['code'], - 'name' => $pType['name'] - ); - } - } - } - catch (CurlException $e) { - error_log('paymentTypesList: connection error', 3, _PS_ROOT_DIR . "log/retailcrm.log"); - } - catch (InvalidJsonException $e) { - error_log('paymentTypesList: ' . $e->getMessage(), 3, _PS_ROOT_DIR . "log/retailcrm.log"); - } - - return $crmPaymentTypes; - } -} diff --git a/retailcrm/lib/custom/.gitkeep b/retailcrm/lib/custom/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/retailcrm/lib/vendor/Icml.php b/retailcrm/lib/vendor/Icml.php deleted file mode 100644 index 2532c2b..0000000 --- a/retailcrm/lib/vendor/Icml.php +++ /dev/null @@ -1,137 +0,0 @@ -shop = $shop; - $this->file = $file; - - $this->properties = array( - 'name', - 'productName', - 'initialPrice', - 'purchasePrice', - 'vendor', - 'picture', - 'url', - 'xmlId', - 'productActivity' - ); - - $this->params = array( - 'article' => 'Артикул', - 'color' => 'Цвет', - 'weight' => 'Вес', - 'size' => 'Размер', - 'tax' => 'Наценка' - ); - } - - public function generate($categories, $offers) - { - $string = ' - - - '.$this->shop.' - - - - - '; - - $xml = new SimpleXMLElement( - $string, - LIBXML_NOENT |LIBXML_NOCDATA | LIBXML_COMPACT | LIBXML_PARSEHUGE - ); - - $this->dd = new DOMDocument(); - $this->dd->preserveWhiteSpace = false; - $this->dd->formatOutput = true; - $this->dd->loadXML($xml->asXML()); - - $this->eCategories = $this->dd - ->getElementsByTagName('categories')->item(0); - $this->eOffers = $this->dd - ->getElementsByTagName('offers')->item(0); - - $this->addCategories($categories); - $this->addOffers($offers); - - $this->dd->saveXML(); - $this->dd->save($this->file); - } - - private function addCategories($categories) - { - foreach($categories as $category) { - $e = $this->eCategories->appendChild( - $this->dd->createElement( - 'category', $category['name'] - ) - ); - - $e->setAttribute('id', $category['id']); - - if ($category['parentId'] > 0) { - $e->setAttribute('parentId', $category['parentId']); - } - } - } - - private function addOffers($offers) - { - foreach ($offers as $offer) { - - $e = $this->eOffers->appendChild( - $this->dd->createElement('offer') - ); - - $e->setAttribute('id', $offer['id']); - $e->setAttribute('productId', $offer['productId']); - - if (!empty($offer['quantity'])) { - $e->setAttribute('quantity', (int) $offer['quantity']); - } else { - $e->setAttribute('quantity', 0); - } - - foreach ($offer['categoryId'] as $categoryId) { - $e->appendChild( - $this->dd->createElement('categoryId', $categoryId) - ); - } - - $offerKeys = array_keys($offer); - - foreach ($offerKeys as $key) { - if (in_array($key, $this->properties)) { - $e->appendChild( - $this->dd->createElement($key) - )->appendChild( - $this->dd->createTextNode($offer[$key]) - ); - } - - if (in_array($key, array_keys($this->params))) { - $param = $this->dd->createElement('param'); - $param->setAttribute('code', $key); - $param->setAttribute('name', $this->params[$key]); - $param->appendChild( - $this->dd->createTextNode($offer[$key]) - ); - $e->appendChild($param); - } - } - } - } -} - diff --git a/retailcrm/lib/vendor/Retailcrm.php b/retailcrm/lib/vendor/Retailcrm.php deleted file mode 100644 index 4276019..0000000 --- a/retailcrm/lib/vendor/Retailcrm.php +++ /dev/null @@ -1,1055 +0,0 @@ -url = $url; - $this->defaultParameters = $defaultParameters; - $this->retry = 0; - } - - /** - * Make HTTP request - * - * @param string $path - * @param string $method (default: 'GET') - * @param array $parameters (default: array()) - * @param int $timeout - * @param bool $verify - * @param bool $debug - * @return ApiResponse - */ - public function makeRequest( - $path, - $method, - array $parameters = array(), - $timeout = 30, - $verify = false, - $debug = false - ) { - $allowedMethods = array(self::METHOD_GET, self::METHOD_POST); - if (!in_array($method, $allowedMethods)) { - throw new InvalidArgumentException(sprintf( - 'Method "%s" is not valid. Allowed methods are %s', - $method, - implode(', ', $allowedMethods) - )); - } - - $parameters = array_merge($this->defaultParameters, $parameters); - - $url = $this->url . $path; - - if (self::METHOD_GET === $method && sizeof($parameters)) { - $url .= '?' . http_build_query($parameters); - } - - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); - curl_setopt($ch, CURLOPT_FAILONERROR, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $verify); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, $verify); - - if (!$debug) { - curl_setopt($ch, CURLOPT_TIMEOUT, (int) $timeout); - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, (int) $timeout); - } else { - curl_setopt($ch, CURLOPT_TIMEOUT_MS, (int) $timeout + ($this->retry * 2000)); - } - - if (self::METHOD_POST === $method) { - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_POSTFIELDS, $parameters); - } - - $responseBody = curl_exec($ch); - $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - $errno = curl_errno($ch); - $error = curl_error($ch); - - curl_close($ch); - - if ($errno && in_array($errno, array(6, 7, 28, 34, 35)) && $this->retry < 3) { - $errno = null; - $error = null; - $this->retry += 1; - $this->makeRequest( - $path, - $method, - $parameters, - $timeout, - $verify, - $debug - ); - } - - if ($errno) { - throw new CurlException($error, $errno); - } - - return new ApiResponse($statusCode, $responseBody); - } - - public function getRetry() - { - return $this->retry; - } -} - -/** - * Response from retailCRM API - */ -class ApiResponse implements ArrayAccess -{ - // HTTP response status code - protected $statusCode; - - // response assoc array - protected $response; - - public function __construct($statusCode, $responseBody = null) - { - $this->statusCode = (int) $statusCode; - - if (!empty($responseBody)) { - $response = json_decode($responseBody, true); - - if (!$response && JSON_ERROR_NONE !== ($error = json_last_error())) { - throw new InvalidJsonException( - "Invalid JSON in the API response body. Error code #$error", - $error - ); - } - - $this->response = $response; - } - } - - /** - * Return HTTP response status code - * - * @return int - */ - public function getStatusCode() - { - return $this->statusCode; - } - - /** - * HTTP request was successful - * - * @return bool - */ - public function isSuccessful() - { - return $this->statusCode < 400; - } - - /** - * Allow to access for the property throw class method - * - * @param string $name - * @return mixed - */ - public function __call($name, $arguments) - { - // convert getSomeProperty to someProperty - $propertyName = strtolower(substr($name, 3, 1)) . substr($name, 4); - - if (!isset($this->response[$propertyName])) { - throw new InvalidArgumentException("Method \"$name\" not found"); - } - - return $this->response[$propertyName]; - } - - /** - * Allow to access for the property throw object property - * - * @param string $name - * @return mixed - */ - public function __get($name) - { - if (!isset($this->response[$name])) { - throw new InvalidArgumentException("Property \"$name\" not found"); - } - - return $this->response[$name]; - } - - /** - * @param mixed $offset - * @param mixed $value - */ - public function offsetSet($offset, $value) - { - throw new BadMethodCallException('This activity not allowed'); - } - - /** - * @param mixed $offset - */ - public function offsetUnset($offset) - { - throw new BadMethodCallException('This call not allowed'); - } - - /** - * @param mixed $offset - * @return bool - */ - public function offsetExists($offset) - { - return isset($this->response[$offset]); - } - - /** - * @param mixed $offset - * @return mixed - */ - public function offsetGet($offset) - { - if (!isset($this->response[$offset])) { - throw new InvalidArgumentException("Property \"$offset\" not found"); - } - - return $this->response[$offset]; - } -} - - -/** - * retailCRM API client class - */ -class ApiClient -{ - const VERSION = 'v3'; - - protected $client; - - /** - * Site code - */ - protected $siteCode; - - /** - * Client creating - * - * @param string $url - * @param string $apiKey - * @param string $siteCode - * @return mixed - */ - public function __construct($url, $apiKey, $site = null) - { - if ('/' != substr($url, strlen($url) - 1, 1)) { - $url .= '/'; - } - - $url = $url . 'api/' . self::VERSION; - - $this->client = new Client($url, array('apiKey' => $apiKey)); - $this->siteCode = $site; - } - - /** - * Create a order - * - * @param array $order - * @param string $site (default: null) - * @return ApiResponse - */ - public function ordersCreate(array $order, $site = null) - { - if (!sizeof($order)) { - throw new InvalidArgumentException('Parameter `order` must contains a data'); - } - - return $this->client->makeRequest("/orders/create", Client::METHOD_POST, $this->fillSite($site, array( - 'order' => json_encode($order) - ))); - } - - /** - * Edit a order - * - * @param array $order - * @param string $site (default: null) - * @return ApiResponse - */ - public function ordersEdit(array $order, $by = 'externalId', $site = null) - { - if (!sizeof($order)) { - throw new InvalidArgumentException('Parameter `order` must contains a data'); - } - - $this->checkIdParameter($by); - - if (!isset($order[$by])) { - throw new InvalidArgumentException(sprintf('Order array must contain the "%s" parameter.', $by)); - } - - return $this->client->makeRequest( - "/orders/" . $order[$by] . "/edit", - Client::METHOD_POST, - $this->fillSite($site, array( - 'order' => json_encode($order), - 'by' => $by, - )) - ); - } - - /** - * Upload array of the orders - * - * @param array $orders - * @param string $site (default: null) - * @return ApiResponse - */ - public function ordersUpload(array $orders, $site = null) - { - if (!sizeof($orders)) { - throw new InvalidArgumentException('Parameter `orders` must contains array of the orders'); - } - - return $this->client->makeRequest("/orders/upload", Client::METHOD_POST, $this->fillSite($site, array( - 'orders' => json_encode($orders), - ))); - } - - /** - * Get order by id or externalId - * - * @param string $id - * @param string $by (default: 'externalId') - * @param string $site (default: null) - * @return ApiResponse - */ - public function ordersGet($id, $by = 'externalId', $site = null) - { - $this->checkIdParameter($by); - - return $this->client->makeRequest("/orders/$id", Client::METHOD_GET, $this->fillSite($site, array( - 'by' => $by - ))); - } - - /** - * Returns a orders history - * - * @param DateTime $startDate (default: null) - * @param DateTime $endDate (default: null) - * @param int $limit (default: 100) - * @param int $offset (default: 0) - * @param bool $skipMyChanges (default: true) - * @return ApiResponse - */ - public function ordersHistory( - DateTime $startDate = null, - DateTime $endDate = null, - $limit = 100, - $offset = 0, - $skipMyChanges = true - ) { - $parameters = array(); - - if ($startDate) { - $parameters['startDate'] = $startDate->format('Y-m-d H:i:s'); - } - if ($endDate) { - $parameters['endDate'] = $endDate->format('Y-m-d H:i:s'); - } - if ($limit) { - $parameters['limit'] = (int) $limit; - } - if ($offset) { - $parameters['offset'] = (int) $offset; - } - if ($skipMyChanges) { - $parameters['skipMyChanges'] = (bool) $skipMyChanges; - } - - return $this->client->makeRequest('/orders/history', Client::METHOD_GET, $parameters); - } - - /** - * Returns filtered orders list - * - * @param array $filter (default: array()) - * @param int $page (default: null) - * @param int $limit (default: null) - * @return ApiResponse - */ - public function ordersList(array $filter = array(), $page = null, $limit = null) - { - $parameters = array(); - - if (sizeof($filter)) { - $parameters['filter'] = $filter; - } - if (null !== $page) { - $parameters['page'] = (int) $page; - } - if (null !== $limit) { - $parameters['limit'] = (int) $limit; - } - - return $this->client->makeRequest('/orders', Client::METHOD_GET, $parameters); - } - - /** - * Returns statuses of the orders - * - * @param array $ids (default: array()) - * @param array $externalIds (default: array()) - * @return ApiResponse - */ - public function ordersStatuses(array $ids = array(), array $externalIds = array()) - { - $parameters = array(); - - if (sizeof($ids)) { - $parameters['ids'] = $ids; - } - if (sizeof($externalIds)) { - $parameters['externalIds'] = $externalIds; - } - - return $this->client->makeRequest('/orders/statuses', Client::METHOD_GET, $parameters); - } - - /** - * Save order IDs' (id and externalId) association in the CRM - * - * @param array $ids - * @return ApiResponse - */ - public function ordersFixExternalIds(array $ids) - { - if (!sizeof($ids)) { - throw new InvalidArgumentException('Method parameter must contains at least one IDs pair'); - } - - return $this->client->makeRequest("/orders/fix-external-ids", Client::METHOD_POST, array( - 'orders' => json_encode($ids), - )); - } - - /** - * Get orders assembly history - * - * @param array $filter (default: array()) - * @param int $page (default: null) - * @param int $limit (default: null) - * @return ApiResponse - */ - public function ordersPacksHistory(array $filter = array(), $page = null, $limit = null) - { - $parameters = array(); - - if (sizeof($filter)) { - $parameters['filter'] = $filter; - } - if (null !== $page) { - $parameters['page'] = (int) $page; - } - if (null !== $limit) { - $parameters['limit'] = (int) $limit; - } - - return $this->client->makeRequest('/orders/packs/history', Client::METHOD_GET, $parameters); - } - - /** - * Create a customer - * - * @param array $customer - * @param string $site (default: null) - * @return ApiResponse - */ - public function customersCreate(array $customer, $site = null) - { - if (!sizeof($customer)) { - throw new InvalidArgumentException('Parameter `customer` must contains a data'); - } - - return $this->client->makeRequest("/customers/create", Client::METHOD_POST, $this->fillSite($site, array( - 'customer' => json_encode($customer) - ))); - } - - /** - * Edit a customer - * - * @param array $customer - * @param string $by (default: 'externalId') - * @param string $site (default: null) - * @return ApiResponse - */ - public function customersEdit(array $customer, $by = 'externalId', $site = null) - { - if (!sizeof($customer)) { - throw new InvalidArgumentException('Parameter `customer` must contains a data'); - } - - $this->checkIdParameter($by); - - if (!isset($customer[$by])) { - throw new InvalidArgumentException(sprintf('Customer array must contain the "%s" parameter.', $by)); - } - - return $this->client->makeRequest( - "/customers/" . $customer[$by] . "/edit", - Client::METHOD_POST, - $this->fillSite( - $site, - array( - 'customer' => json_encode($customer), - 'by' => $by - ) - ) - ); - } - - /** - * Upload array of the customers - * - * @param array $customers - * @param string $site (default: null) - * @return ApiResponse - */ - public function customersUpload(array $customers, $site = null) - { - if (!sizeof($customers)) { - throw new InvalidArgumentException('Parameter `customers` must contains array of the customers'); - } - - return $this->client->makeRequest("/customers/upload", Client::METHOD_POST, $this->fillSite($site, array( - 'customers' => json_encode($customers), - ))); - } - - /** - * Get customer by id or externalId - * - * @param string $id - * @param string $by (default: 'externalId') - * @param string $site (default: null) - * @return ApiResponse - */ - public function customersGet($id, $by = 'externalId', $site = null) - { - $this->checkIdParameter($by); - - return $this->client->makeRequest("/customers/$id", Client::METHOD_GET, $this->fillSite($site, array( - 'by' => $by - ))); - } - - /** - * Returns filtered customers list - * - * @param array $filter (default: array()) - * @param int $page (default: null) - * @param int $limit (default: null) - * @return ApiResponse - */ - public function customersList(array $filter = array(), $page = null, $limit = null) - { - $parameters = array(); - - if (sizeof($filter)) { - $parameters['filter'] = $filter; - } - if (null !== $page) { - $parameters['page'] = (int) $page; - } - if (null !== $limit) { - $parameters['limit'] = (int) $limit; - } - - return $this->client->makeRequest('/customers', Client::METHOD_GET, $parameters); - } - - /** - * Save customer IDs' (id and externalId) association in the CRM - * - * @param array $ids - * @return ApiResponse - */ - public function customersFixExternalIds(array $ids) - { - if (!sizeof($ids)) { - throw new InvalidArgumentException('Method parameter must contains at least one IDs pair'); - } - - return $this->client->makeRequest("/customers/fix-external-ids", Client::METHOD_POST, array( - 'customers' => json_encode($ids), - )); - } - - /** - * Get purchace prices & stock balance - * - * @param array $filter (default: array()) - * @param int $page (default: null) - * @param int $limit (default: null) - * @param string $site (default: null) - * @return ApiResponse - */ - public function storeInventories(array $filter = array(), $page = null, $limit = null, $site = null) - { - $parameters = array(); - - if (sizeof($filter)) { - $parameters['filter'] = $filter; - } - if (null !== $page) { - $parameters['page'] = (int) $page; - } - if (null !== $limit) { - $parameters['limit'] = (int) $limit; - } - - return $this->client->makeRequest('/store/inventories', Client::METHOD_GET, $this->fillSite($site, $parameters)); - } - - /** - * Upload store inventories - * - * @param array $offers - * @param string $site (default: null) - * @return ApiResponse - */ - public function storeInventoriesUpload(array $offers, $site = null) - { - if (!sizeof($offers)) { - throw new InvalidArgumentException('Parameter `offers` must contains array of the customers'); - } - - return $this->client->makeRequest( - "/store/inventories/upload", - Client::METHOD_POST, - $this->fillSite($site, array('offers' => json_encode($offers))) - ); - } - - /** - * Returns deliveryServices list - * - * @return ApiResponse - */ - public function deliveryServicesList() - { - return $this->client->makeRequest('/reference/delivery-services', Client::METHOD_GET); - } - - /** - * Returns deliveryTypes list - * - * @return ApiResponse - */ - public function deliveryTypesList() - { - return $this->client->makeRequest('/reference/delivery-types', Client::METHOD_GET); - } - - /** - * Returns orderMethods list - * - * @return ApiResponse - */ - public function orderMethodsList() - { - return $this->client->makeRequest('/reference/order-methods', Client::METHOD_GET); - } - - /** - * Returns orderTypes list - * - * @return ApiResponse - */ - public function orderTypesList() - { - return $this->client->makeRequest('/reference/order-types', Client::METHOD_GET); - } - - /** - * Returns paymentStatuses list - * - * @return ApiResponse - */ - public function paymentStatusesList() - { - return $this->client->makeRequest('/reference/payment-statuses', Client::METHOD_GET); - } - - /** - * Returns paymentTypes list - * - * @return ApiResponse - */ - public function paymentTypesList() - { - return $this->client->makeRequest('/reference/payment-types', Client::METHOD_GET); - } - - /** - * Returns productStatuses list - * - * @return ApiResponse - */ - public function productStatusesList() - { - return $this->client->makeRequest('/reference/product-statuses', Client::METHOD_GET); - } - - /** - * Returns statusGroups list - * - * @return ApiResponse - */ - public function statusGroupsList() - { - return $this->client->makeRequest('/reference/status-groups', Client::METHOD_GET); - } - - /** - * Returns statuses list - * - * @return ApiResponse - */ - public function statusesList() - { - return $this->client->makeRequest('/reference/statuses', Client::METHOD_GET); - } - - /** - * Returns sites list - * - * @return ApiResponse - */ - public function sitesList() - { - return $this->client->makeRequest('/reference/sites', Client::METHOD_GET); - } - - /** - * Returns stores list - * - * @return ApiResponse - */ - public function storesList() - { - return $this->client->makeRequest('/reference/stores', Client::METHOD_GET); - } - - /** - * Edit deliveryService - * - * @param array $data delivery service data - * @return ApiResponse - */ - public function deliveryServicesEdit(array $data) - { - if (!isset($data['code'])) { - throw new InvalidArgumentException('Data must contain "code" parameter.'); - } - - return $this->client->makeRequest( - '/reference/delivery-services/' . $data['code'] . '/edit', - Client::METHOD_POST, - array( - 'deliveryService' => json_encode($data) - ) - ); - } - - /** - * Edit deliveryType - * - * @param array $data delivery type data - * @return ApiResponse - */ - public function deliveryTypesEdit(array $data) - { - if (!isset($data['code'])) { - throw new InvalidArgumentException('Data must contain "code" parameter.'); - } - - return $this->client->makeRequest( - '/reference/delivery-types/' . $data['code'] . '/edit', - Client::METHOD_POST, - array( - 'deliveryType' => json_encode($data) - ) - ); - } - - /** - * Edit orderMethod - * - * @param array $data order method data - * @return ApiResponse - */ - public function orderMethodsEdit(array $data) - { - if (!isset($data['code'])) { - throw new InvalidArgumentException('Data must contain "code" parameter.'); - } - - return $this->client->makeRequest( - '/reference/order-methods/' . $data['code'] . '/edit', - Client::METHOD_POST, - array( - 'orderMethod' => json_encode($data) - ) - ); - } - - /** - * Edit orderType - * - * @param array $data order type data - * @return ApiResponse - */ - public function orderTypesEdit(array $data) - { - if (!isset($data['code'])) { - throw new InvalidArgumentException('Data must contain "code" parameter.'); - } - - return $this->client->makeRequest( - '/reference/order-types/' . $data['code'] . '/edit', - Client::METHOD_POST, - array( - 'orderType' => json_encode($data) - ) - ); - } - - /** - * Edit paymentStatus - * - * @param array $data payment status data - * @return ApiResponse - */ - public function paymentStatusesEdit(array $data) - { - if (!isset($data['code'])) { - throw new InvalidArgumentException('Data must contain "code" parameter.'); - } - - return $this->client->makeRequest( - '/reference/payment-statuses/' . $data['code'] . '/edit', - Client::METHOD_POST, - array( - 'paymentStatus' => json_encode($data) - ) - ); - } - - /** - * Edit paymentType - * - * @param array $data payment type data - * @return ApiResponse - */ - public function paymentTypesEdit(array $data) - { - if (!isset($data['code'])) { - throw new InvalidArgumentException('Data must contain "code" parameter.'); - } - - return $this->client->makeRequest( - '/reference/payment-types/' . $data['code'] . '/edit', - Client::METHOD_POST, - array( - 'paymentType' => json_encode($data) - ) - ); - } - - /** - * Edit productStatus - * - * @param array $data product status data - * @return ApiResponse - */ - public function productStatusesEdit(array $data) - { - if (!isset($data['code'])) { - throw new InvalidArgumentException('Data must contain "code" parameter.'); - } - - return $this->client->makeRequest( - '/reference/product-statuses/' . $data['code'] . '/edit', - Client::METHOD_POST, - array( - 'productStatus' => json_encode($data) - ) - ); - } - - /** - * Edit order status - * - * @param array $data status data - * @return ApiResponse - */ - public function statusesEdit(array $data) - { - if (!isset($data['code'])) { - throw new InvalidArgumentException('Data must contain "code" parameter.'); - } - - return $this->client->makeRequest( - '/reference/statuses/' . $data['code'] . '/edit', - Client::METHOD_POST, - array( - 'status' => json_encode($data) - ) - ); - } - - /** - * Edit site - * - * @param array $data site data - * @return ApiResponse - */ - public function sitesEdit(array $data) - { - if (!isset($data['code'])) { - throw new InvalidArgumentException('Data must contain "code" parameter.'); - } - - return $this->client->makeRequest( - '/reference/sites/' . $data['code'] . '/edit', - Client::METHOD_POST, - array( - 'site' => json_encode($data) - ) - ); - } - - /** - * Edit store - * - * @param array $data site data - * @return ApiResponse - */ - public function storesEdit(array $data) - { - if (!isset($data['code'])) { - throw new InvalidArgumentException('Data must contain "code" parameter.'); - } - - if (!isset($data['name'])) { - throw new InvalidArgumentException('Data must contain "name" parameter.'); - } - - return $this->client->makeRequest( - '/reference/stores/' . $data['code'] . '/edit', - Client::METHOD_POST, - array( - 'store' => json_encode($data) - ) - ); - } - - /** - * Update CRM basic statistic - * - * @return ApiResponse - */ - public function statisticUpdate() - { - return $this->client->makeRequest('/statistic/update', Client::METHOD_GET); - } - - /** - * Return current site - * - * @return string - */ - public function getSite() - { - return $this->siteCode; - } - - /** - * Set site - * - * @param string $site - * @return void - */ - public function setSite($site) - { - $this->siteCode = $site; - } - - /** - * Check ID parameter - * - * @param string $by - * @return bool - */ - protected function checkIdParameter($by) - { - $allowedForBy = array('externalId', 'id'); - if (!in_array($by, $allowedForBy)) { - throw new InvalidArgumentException(sprintf( - 'Value "%s" for parameter "by" is not valid. Allowed values are %s.', - $by, - implode(', ', $allowedForBy) - )); - } - - return true; - } - - /** - * Fill params by site value - * - * @param string $site - * @param array $params - * @return array - */ - protected function fillSite($site, array $params) - { - if ($site) { - $params['site'] = $site; - } elseif ($this->siteCode) { - $params['site'] = $this->siteCode; - } - - return $params; - } -} - -class InvalidJsonException extends DomainException -{ -} - -class CurlException extends RuntimeException -{ -} diff --git a/retailcrm/lib/vendor/Service.php b/retailcrm/lib/vendor/Service.php deleted file mode 100644 index 98d13a0..0000000 --- a/retailcrm/lib/vendor/Service.php +++ /dev/null @@ -1,43 +0,0 @@ --(Qd3<3av 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 = 'retailcrm'; + $this->tab = 'export'; + $this->version = '2.0'; + $this->author = 'Retail Driver LCC'; + $this->displayName = $this->l('RetailCRM'); + $this->description = $this->l('Integration module for RetailCRM'); + $this->confirmUninstall = $this->l('Are you sure you want to uninstall?'); + $this->default_lang = (int)Configuration::get('PS_LANG_DEFAULT'); + $this->default_currency = (int)Configuration::get('PS_CURRENCY_DEFAULT'); + $this->default_country = (int)Configuration::get('PS_COUNTRY_DEFAULT'); + $this->apiUrl = Configuration::get('RETAILCRM_ADDRESS'); + $this->apiKey = Configuration::get('RETAILCRM_API_TOKEN'); + $this->ps_versions_compliancy = array('min' => '1.5', 'max' => _PS_VERSION_); + $this->version = substr(_PS_VERSION_, 0, 3); + + if ($this->version == '1.6') { + $this->bootstrap = true; + } + + if (!empty($this->apiUrl) && !empty($this->apiKey)) { + $this->api = new RetailcrmProxy($this->apiUrl, $this->apiKey, _PS_ROOT_DIR_ . '/retailcrm.log'); + $this->reference = new RetailcrmReferences($this->api); + } + + parent::__construct(); + } + + function install() + { + return ( + parent::install() && + $this->registerHook('newOrder') && + $this->registerHook('actionOrderStatusPostUpdate') && + $this->registerHook('actionPaymentConfirmation') && + $this->registerHook('actionCustomerAccountAdd') + ); + } + + function uninstall() + { + return parent::uninstall() && + Configuration::deleteByName('RETAILCRM_ADDRESS') && + Configuration::deleteByName('RETAILCRM_API_TOKEN') && + Configuration::deleteByName('RETAILCRM_API_STATUS') && + Configuration::deleteByName('RETAILCRM_API_DELIVERY') && + Configuration::deleteByName('RETAILCRM_LAST_SYNC') && + Configuration::deleteByName('RETAILCRM_API_ADDR'); + } + + public function getContent() + { + $output = null; + + $address = Configuration::get('RETAILCRM_ADDRESS'); + $token = Configuration::get('RETAILCRM_API_TOKEN'); + + if (!$address || $address == '') { + $output .= $this->displayError($this->l('Invalid or empty crm address')); + } elseif (!$token || $token == '') { + $output .= $this->displayError($this->l('Invalid or empty crm api token')); + } else { + $output .= $this->displayConfirmation( + $this->l('Timezone settings must be identical to both of your crm and shop') . + " $address/admin/settings#t-main" + ); + } + + if (Tools::isSubmit('submit' . $this->name)) { + $address = strval(Tools::getValue('RETAILCRM_ADDRESS')); + $token = strval(Tools::getValue('RETAILCRM_API_TOKEN')); + $delivery = json_encode(Tools::getValue('RETAILCRM_API_DELIVERY')); + $status = json_encode(Tools::getValue('RETAILCRM_API_STATUS')); + $payment = json_encode(Tools::getValue('RETAILCRM_API_PAYMENT')); + $order_address = json_encode(Tools::getValue('RETAILCRM_API_ADDR')); + + if (!$address || empty($address) || !Validate::isGenericName($address)) { + $output .= $this->displayError($this->l('Invalid crm address')); + } elseif (!$token || empty($token) || !Validate::isGenericName($token)) { + $output .= $this->displayError($this->l('Invalid crm api token')); + } else { + Configuration::updateValue('RETAILCRM_ADDRESS', $address); + Configuration::updateValue('RETAILCRM_API_TOKEN', $token); + Configuration::updateValue('RETAILCRM_API_DELIVERY', $delivery); + Configuration::updateValue('RETAILCRM_API_STATUS', $status); + Configuration::updateValue('RETAILCRM_API_PAYMENT', $payment); + Configuration::updateValue('RETAILCRM_API_ADDR', $order_address); + $output .= $this->displayConfirmation($this->l('Settings updated')); + } + } + + $this->display(__FILE__, 'retailcrm.tpl'); + + return $output . $this->displayForm(); + } + + public function displayForm() + { + + $this->displayConfirmation($this->l('Settings updated')); + + $default_lang = $this->default_lang; + + /* + * Network connection form + */ + $fields_form[0]['form'] = array( + 'legend' => array( + 'title' => $this->l('Network connection'), + ), + 'input' => array( + array( + 'type' => 'text', + 'label' => $this->l('CRM address'), + 'name' => 'RETAILCRM_ADDRESS', + 'size' => 20, + 'required' => true + ), + array( + 'type' => 'text', + 'label' => $this->l('CRM token'), + 'name' => 'RETAILCRM_API_TOKEN', + 'size' => 20, + 'required' => true + ) + ), + 'submit' => array( + 'title' => $this->l('Save'), + 'class' => 'button' + ) + ); + + + if (!empty($this->apiUrl) && !empty($this->apiKey)) { + /* + * Delivery + */ + $fields_form[1]['form'] = array( + 'legend' => array('title' => $this->l('Delivery')), + 'input' => $this->reference->getDeliveryTypes(), + ); + + /* + * Order status + */ + $fields_form[2]['form'] = array( + 'legend' => array('title' => $this->l('Order statuses')), + 'input' => $this->reference->getStatuses(), + ); + + /* + * Payment + */ + $fields_form[3]['form'] = array( + 'legend' => array('title' => $this->l('Payment types')), + 'input' => $this->reference->getPaymentTypes(), + ); + } + + /* + * Address fields + */ +// $fields_form[4]['form'] = array( +// 'legend' => array('title' => $this->l('Address')), +// 'input' => $this->getAddressFields() +// ); + + /* + * Diplay forms + */ + + $helper = new HelperForm(); + + $helper->module = $this; + $helper->name_controller = $this->name; + $helper->token = Tools::getAdminTokenLite('AdminModules'); + $helper->currentIndex = AdminController::$currentIndex . '&configure=' . $this->name; + + $helper->default_form_language = $default_lang; + $helper->allow_employee_form_lang = $default_lang; + + $helper->title = $this->displayName; + $helper->show_toolbar = true; + $helper->toolbar_scroll = true; + $helper->submit_action = 'submit' . $this->name; + $helper->toolbar_btn = array( + 'save' => + array( + 'desc' => $this->l('Save'), + 'href' => sprintf( + "%s&configure=%s&save%s&token=%s", + AdminController::$currentIndex, + $this->name, + $this->name, + Tools::getAdminTokenLite('AdminModules') + ) + ), + 'back' => array( + 'href' => AdminController::$currentIndex . '&token=' . Tools::getAdminTokenLite('AdminModules'), + 'desc' => $this->l('Back to list') + ) + ); + + $helper->fields_value['RETAILCRM_ADDRESS'] = Configuration::get('RETAILCRM_ADDRESS'); + $helper->fields_value['RETAILCRM_API_TOKEN'] = Configuration::get('RETAILCRM_API_TOKEN'); + + $deliverySettings = Configuration::get('RETAILCRM_API_DELIVERY'); + if (isset($deliverySettings) && $deliverySettings != '') { + $deliveryTypes = json_decode($deliverySettings); + if ($deliveryTypes) { + foreach ($deliveryTypes as $idx => $delivery) { + $name = 'RETAILCRM_API_DELIVERY[' . $idx . ']'; + $helper->fields_value[$name] = $delivery; + } + } + } + + $statusSettings = Configuration::get('RETAILCRM_API_STATUS'); + if (isset($statusSettings) && $statusSettings != '') { + $statusTypes = json_decode($statusSettings); + if ($statusTypes) { + foreach ($statusTypes as $idx => $status) { + $name = 'RETAILCRM_API_STATUS[' . $idx . ']'; + $helper->fields_value[$name] = $status; + } + } + } + + $paymentSettings = Configuration::get('RETAILCRM_API_PAYMENT'); + if (isset($paymentSettings) && $paymentSettings != '') { + $paymentTypes = json_decode($paymentSettings); + if ($paymentTypes) { + foreach ($paymentTypes as $idx => $payment) { + $name = 'RETAILCRM_API_PAYMENT[' . $idx . ']'; + $helper->fields_value[$name] = $payment; + } + } + } + +// $addressSettings = Configuration::get('RETAILCRM_API_ADDR'); +// +// if (isset($addressSettings) && $addressSettings != '') { +// $addressTypes = json_decode($addressSettings); +// foreach ($addressTypes as $idx => $address) { +// $name = 'RETAILCRM_API_ADDR[' . $idx . ']'; +// $helper->fields_value[$name] = $address; +// } +// } + + return $helper->generateForm($fields_form); + } + +// public function getAddressFields() +// { +// $addressFields = array(); +// $address = explode(' ', str_replace("\n", ' ', AddressFormat::getAddressCountryFormat($this->context->country->id))); +// +// if (!empty($address)) { +// foreach ($address as $idx => $a) { +// if (!in_array($a, array('vat_number', 'phone_mobile', 'company'))) { +// if (!strpos($a, ':')) { +// $a = preg_replace('/_/', ' ', $a); +// $a = preg_replace('/[\,\.]/', '', $a); +// $addressFields[] = array( +// 'type' => 'select', +// 'label' => $this->l((string)ucfirst($a)), +// 'name' => 'RETAILCRM_API_ADDR[' . $idx . ']', +// 'required' => false, +// 'options' => array( +// 'query' => array( +// array('name' => '', 'id_option' => ''), +// array('name' => $this->l('FIRST_NAME'), 'id_option' => 'first_name'), +// array('name' => $this->l('LAST_NAME'), 'id_option' => 'last_name'), +// array('name' => $this->l('PHONE'), 'id_option' => 'phone'), +// array('name' => $this->l('EMAIL'), 'id_option' => 'email'), +// array('name' => $this->l('ADDRESS'), 'id_option' => 'address'), +// array('name' => $this->l('COUNTRY'), 'id_option' => 'country'), +// array('name' => $this->l('REGION'), 'id_option' => 'region'), +// array('name' => $this->l('CITY'), 'id_option' => 'city'), +// array('name' => $this->l('ZIP'), 'id_option' => 'index'), +// array('name' => $this->l('STREET'), 'id_option' => 'street'), +// array('name' => $this->l('BUILDING'), 'id_option' => 'building'), +// array('name' => $this->l('FLAT'), 'id_option' => 'flat'), +// array('name' => $this->l('INTERCOMCODE'), 'id_option' => 'intercomcode'), +// array('name' => $this->l('FLOOR'), 'id_option' => 'floor'), +// array('name' => $this->l('BLOCK'), 'id_option' => 'block'), +// array('name' => $this->l('HOUSE'), 'ID' => 'house') +// ), +// 'id' => 'id_option', +// 'name' => 'name' +// ) +// ); +// } +// } +// } +// } +// +// return $addressFields; +// } + + public function hookActionCustomerAccountAdd($params) + { + $this->api->customersCreate( + array( + 'externalId' => $params['newCustomer']->id, + 'firstName' => $params['newCustomer']->firstname, + 'lastName' => $params['newCustomer']->lastname, + 'email' => $params['newCustomer']->email, + 'createdAt' => $params['newCustomer']->date_add + ) + ); + } + + public function hookNewOrder($params) + { + return $this->hookActionOrderStatusPostUpdate($params); + } + + public function hookActionPaymentConfirmation($params) + { + $this->api->ordersEdit( + array( + 'externalId' => $params['id_order'], + 'paymentStatus' => 'paid', + 'createdAt' => $params['cart']->date_upd + ) + ); + + return $this->hookActionOrderStatusPostUpdate($params); + } + + public function hookActionOrderStatusPostUpdate($params) + { + $address_id = Address::getFirstCustomerAddressId($params['cart']->id_customer); + $sql = 'SELECT * FROM ' . _DB_PREFIX_ . 'address WHERE id_address=' . (int)$address_id; + $dbaddress = Db::getInstance()->ExecuteS($sql); + $address = $dbaddress[0]; + $delivery = json_decode(Configuration::get('RETAILCRM_API_DELIVERY')); + $payment = json_decode(Configuration::get('RETAILCRM_API_PAYMENT')); + $inCart = $params['cart']->getProducts(); + + if (isset($params['orderStatus'])) { + $this->api->customersEdit( + array( + 'externalId' => $params['cart']->id_customer, + 'lastName' => $params['customer']->lastname, + 'firstName' => $params['customer']->firstname, + 'email' => $params['customer']->email, + 'phones' => array( + array( + 'number' => $address['phone'], + ), + array( + 'number' => $address['phone_mobile'], + ) + ), + 'createdAt' => $params['customer']->date_add + ) + ); + + $items = array(); + foreach ($inCart as $item) { + $items[] = array( + 'initialPrice' => (!empty($item['rate'])) ? $item['price'] + ($item['price'] * $item['rate'] / 100) : $item['price'], + 'quantity' => $item['quantity'], + 'productId' => $item['id_product'], + 'productName' => $item['name'], + 'createdAt' => $item['date_add'] + ); + } + + $dTypeKey = $params['cart']->id_carrier; + + if (Module::getInstanceByName('advancedcheckout') === false) { + $pTypeKey = $params['order']->module; + } else { + $pTypeKey = $params['order']->payment; + } + + $this->api->ordersCreate( + array( + 'externalId' => $params['order']->id, + 'orderType' => 'eshop-individual', + 'orderMethod' => 'shopping-cart', + 'status' => 'new', + 'customerId' => $params['cart']->id_customer, + 'firstName' => $params['customer']->firstname, + 'lastName' => $params['customer']->lastname, + 'phone' => $address['phone'], + 'email' => $params['customer']->email, + 'paymentType' => $payment->$pTypeKey, + 'delivery' => array( + 'code' => $delivery->$dTypeKey, + 'cost' => $params['order']->total_shipping, + 'address' => array( + 'city' => $address['city'], + 'index' => $address['postcode'], + 'text' => $address['address1'], + ) + ), + 'discount' => $params['order']->total_discounts, + 'items' => $items, + 'createdAt' => $params['order']->date_add + ) + ); + } + + if (!empty($params['newOrderStatus'])) { + $statuses = OrderState::getOrderStates($this->default_lang); + $aStatuses = json_decode(Configuration::get('RETAILCRM_API_STATUS')); + foreach ($statuses as $status) { + if ($status['name'] == $params['newOrderStatus']->name) { + $currStatus = $status['id_order_state']; + $this->api->ordersEdit( + array( + 'externalId' => $params['id_order'], + 'status' => $aStatuses->$currStatus + ) + ); + } + } + } + } +} diff --git a/retailcrm/version.1.6.php b/retailcrm/version.1.6.php deleted file mode 100644 index 188ac27..0000000 --- a/retailcrm/version.1.6.php +++ /dev/null @@ -1,433 +0,0 @@ -name = 'retailcrm'; - $this->tab = 'market_place'; - $this->version = '1.1'; - $this->author = 'Retail Driver LCC'; - $this->displayName = $this->l('RetailCRM'); - $this->description = $this->l('Integration module for RetailCRM'); - $this->confirmUninstall = $this->l('Are you sure you want to uninstall?'); - - $this->apiUrl = Configuration::get('RETAILCRM_ADDRESS'); - $this->apiKey = Configuration::get('RETAILCRM_API_TOKEN'); - - if (!empty($this->apiUrl) && !empty($this->apiKey)) { - $this->api = new RetailcrmProxy($this->apiUrl, $this->apiKey, _PS_ROOT_DIR_ . '/retailcrm.log'); - $this->reference = new RetailcrmReferences($this->api); - } - $this->default_lang = (int)Configuration::get('PS_LANG_DEFAULT'); - $this->default_currency = (int)Configuration::get('PS_CURRENCY_DEFAULT'); - $this->default_country = (int)Configuration::get('PS_COUNTRY_DEFAULT'); - $this->response = array(); - $this->customerFix = array(); - $this->orderFix = array(); - $this->address_id = null; - $this->customer_id = null; - $this->customer = null; - parent::__construct(); - } - - function install() - { - return ( - parent::install() && - $this->registerHook('newOrder') && - $this->registerHook('actionOrderStatusPostUpdate') && - $this->registerHook('actionPaymentConfirmation') && - $this->registerHook('actionCustomerAccountAdd') - ); - } - - function uninstall() - { - return parent::uninstall() && - Configuration::deleteByName('RETAILCRM_ADDRESS') && - Configuration::deleteByName('RETAILCRM_API_TOKEN') && - Configuration::deleteByName('RETAILCRM_API_STATUS') && - Configuration::deleteByName('RETAILCRM_API_DELIVERY') && - Configuration::deleteByName('RETAILCRM_LAST_SYNC') && - Configuration::deleteByName('RETAILCRM_API_ADDR'); - } - - public function getContent() - { - $output = null; - - $address = Configuration::get('RETAILCRM_ADDRESS'); - $token = Configuration::get('RETAILCRM_API_TOKEN'); - - if (!$address || $address == '') { - $output .= $this->displayError($this->l('Invalid or empty crm address')); - } elseif (!$token || $token == '') { - $output .= $this->displayError($this->l('Invalid or empty crm api token')); - } else { - $output .= $this->displayConfirmation( - $this->l('Timezone settings must be identical to both of your crm and shop') . - " $address/admin/settings#t-main" - ); - } - - if (Tools::isSubmit('submit' . $this->name)) { - $address = strval(Tools::getValue('RETAILCRM_ADDRESS')); - $token = strval(Tools::getValue('RETAILCRM_API_TOKEN')); - $delivery = json_encode(Tools::getValue('RETAILCRM_API_DELIVERY')); - $status = json_encode(Tools::getValue('RETAILCRM_API_STATUS')); - $payment = json_encode(Tools::getValue('RETAILCRM_API_PAYMENT')); - $order_address = json_encode(Tools::getValue('RETAILCRM_API_ADDR')); - - if (!$address || empty($address) || !Validate::isGenericName($address)) { - $output .= $this->displayError($this->l('Invalid crm address')); - } elseif (!$token || empty($token) || !Validate::isGenericName($token)) { - $output .= $this->displayError($this->l('Invalid crm api token')); - } else { - Configuration::updateValue('RETAILCRM_ADDRESS', $address); - Configuration::updateValue('RETAILCRM_API_TOKEN', $token); - Configuration::updateValue('RETAILCRM_API_DELIVERY', $delivery); - Configuration::updateValue('RETAILCRM_API_STATUS', $status); - Configuration::updateValue('RETAILCRM_API_PAYMENT', $payment); - Configuration::updateValue('RETAILCRM_API_ADDR', $order_address); - $output .= $this->displayConfirmation($this->l('Settings updated')); - } - } - - $this->display(__FILE__, 'retailcrm.tpl'); - - return $output . $this->displayForm(); - } - - public function displayForm() - { - - $this->displayConfirmation($this->l('Settings updated')); - - $default_lang = $this->default_lang; - $intaroCrm = $this->api; - - /* - * Network connection form - */ - $fields_form[0]['form'] = array( - 'legend' => array( - 'title' => $this->l('Network connection'), - ), - 'input' => array( - array( - 'type' => 'text', - 'label' => $this->l('CRM address'), - 'name' => 'RETAILCRM_ADDRESS', - 'size' => 20, - 'required' => true - ), - array( - 'type' => 'text', - 'label' => $this->l('CRM token'), - 'name' => 'RETAILCRM_API_TOKEN', - 'size' => 20, - 'required' => true - ) - ), - 'submit' => array( - 'title' => $this->l('Save'), - 'class' => 'button' - ) - ); - - - if (!empty($this->apiUrl) && !empty($this->apiKey)) { - /* - * Delivery - */ - $fields_form[1]['form'] = array( - 'legend' => array( - 'title' => $this->l('Delivery'), - ), - 'input' => $this->reference->getDeliveryTypes(), - ); - - /* - * Order status - */ - $fields_form[2]['form'] = array( - 'legend' => array( - 'title' => $this->l('Order statuses'), - ), - 'input' => $this->reference->getStatuses(), - ); - - /* - * Payment - */ - $fields_form[3]['form'] = array( - 'legend' => array( - 'title' => $this->l('Payment types'), - ), - 'input' => $this->reference->getPaymentTypes(), - ); - } - - /* - * Address fields - */ - $fields_form[4]['form'] = array( - 'legend' => array( - 'title' => $this->l('Address'), - ), - 'input' => $this->getAddressFields() - ); - - - /* - * Diplay forms - */ - - $helper = new HelperForm(); - - $helper->module = $this; - $helper->name_controller = $this->name; - $helper->token = Tools::getAdminTokenLite('AdminModules'); - $helper->currentIndex = AdminController::$currentIndex . '&configure=' . $this->name; - - $helper->default_form_language = $default_lang; - $helper->allow_employee_form_lang = $default_lang; - - $helper->title = $this->displayName; - $helper->show_toolbar = true; - $helper->toolbar_scroll = true; - $helper->submit_action = 'submit' . $this->name; - $helper->toolbar_btn = array( - 'save' => - array( - 'desc' => $this->l('Save'), - 'href' => sprintf( - "%s&configure=%s&save%s&token=%s", - AdminController::$currentIndex, - $this->name, - $this->name, - Tools::getAdminTokenLite('AdminModules') - ) - ), - 'back' => array( - 'href' => AdminController::$currentIndex . '&token=' . Tools::getAdminTokenLite('AdminModules'), - 'desc' => $this->l('Back to list') - ) - ); - - $helper->fields_value['RETAILCRM_ADDRESS'] = Configuration::get('RETAILCRM_ADDRESS'); - $helper->fields_value['RETAILCRM_API_TOKEN'] = Configuration::get('RETAILCRM_API_TOKEN'); - - $deliverySettings = Configuration::get('RETAILCRM_API_DELIVERY'); - if (isset($deliverySettings) && $deliverySettings != '') { - $deliveryTypes = json_decode($deliverySettings); - foreach ($deliveryTypes as $idx => $delivery) { - $name = 'RETAILCRM_API_DELIVERY[' . $idx . ']'; - $helper->fields_value[$name] = $delivery; - } - } - - $statusSettings = Configuration::get('RETAILCRM_API_STATUS'); - if (isset($statusSettings) && $statusSettings != '') { - $statusTypes = json_decode($statusSettings); - foreach ($statusTypes as $idx => $status) { - $name = 'RETAILCRM_API_STATUS[' . $idx . ']'; - $helper->fields_value[$name] = $status; - } - } - - $paymentSettings = Configuration::get('RETAILCRM_API_PAYMENT'); - if (isset($paymentSettings) && $paymentSettings != '') { - $paymentTypes = json_decode($paymentSettings); - foreach ($paymentTypes as $idx => $payment) { - $name = 'RETAILCRM_API_PAYMENT[' . $idx . ']'; - $helper->fields_value[$name] = $payment; - } - } - - $addressSettings = Configuration::get('RETAILCRM_API_ADDR'); - if (isset($addressSettings) && $addressSettings != '') { - $addressTypes = json_decode($addressSettings); - foreach ($addressTypes as $idx => $address) { - $name = 'RETAILCRM_API_ADDR[' . $idx . ']'; - $helper->fields_value[$name] = $address; - } - } - - return $helper->generateForm($fields_form); - } - - public function getAddressFields() - { - $addressFields = array(); - $address = explode(' ', str_replace("\n", ' ', AddressFormat::getAddressCountryFormat($this->context->country->id))); - - if (!empty($address)) { - foreach ($address as $idx => $a) { - if (!in_array($a, array('vat_number', 'phone_mobile', 'company'))) { - if (!strpos($a, ':')) { - $a = preg_replace('/_/', ' ', $a); - $a = preg_replace('/[\,\.]/', '', $a); - $addressFields[] = array( - 'type' => 'select', - 'label' => $this->l((string)ucfirst($a)), - 'name' => 'RETAILCRM_API_ADDR[' . $idx . ']', - 'required' => false, - 'options' => array( - 'query' => array( - array('name' => '', 'id_option' => ''), - array('name' => $this->l('FIRST_NAME'), 'id_option' => 'first_name'), - array('name' => $this->l('LAST_NAME'), 'id_option' => 'last_name'), - array('name' => $this->l('PHONE'), 'id_option' => 'phone'), - array('name' => $this->l('EMAIL'), 'id_option' => 'email'), - array('name' => $this->l('ADDRESS'), 'id_option' => 'address'), - array('name' => $this->l('COUNTRY'), 'id_option' => 'country'), - array('name' => $this->l('REGION'), 'id_option' => 'region'), - array('name' => $this->l('CITY'), 'id_option' => 'city'), - array('name' => $this->l('ZIP'), 'id_option' => 'index'), - array('name' => $this->l('STREET'), 'id_option' => 'street'), - array('name' => $this->l('BUILDING'), 'id_option' => 'building'), - array('name' => $this->l('FLAT'), 'id_option' => 'flat'), - array('name' => $this->l('INTERCOMCODE'), 'id_option' => 'intercomcode'), - array('name' => $this->l('FLOOR'), 'id_option' => 'floor'), - array('name' => $this->l('BLOCK'), 'id_option' => 'block'), - array('name' => $this->l('HOUSE'), 'ID' => 'house') - ), - 'id' => 'id_option', - 'name' => 'name' - ) - ); - } - } - } - } - - return $addressFields; - } - - public function hookActionCustomerAccountAdd($params) - { - $this->api->customersCreate( - array( - 'externalId' => $params['newCustomer']->id, - 'firstName' => $params['newCustomer']->firstname, - 'lastName' => $params['newCustomer']->lastname, - 'email' => $params['newCustomer']->email, - 'createdAt' => $params['newCustomer']->date_add - ) - ); - } - - public function hookNewOrder($params) - { - return $this->hookActionOrderStatusPostUpdate($params); - } - - public function hookActionPaymentConfirmation($params) - { - $this->api->ordersEdit( - array( - 'externalId' => $params['id_order'], - 'paymentStatus' => 'paid', - 'createdAt' => $params['cart']->date_upd - ) - ); - - return $this->hookActionOrderStatusPostUpdate($params); - } - - public function hookActionOrderStatusPostUpdate($params) - { - $address_id = Address::getFirstCustomerAddressId($params['cart']->id_customer); - $sql = 'SELECT * FROM ' . _DB_PREFIX_ . 'address WHERE id_address=' . (int)$address_id; - $dbaddress = Db::getInstance()->ExecuteS($sql); - $address = $dbaddress[0]; - $delivery = json_decode(Configuration::get('RETAILCRM_API_DELIVERY')); - $payment = json_decode(Configuration::get('RETAILCRM_API_PAYMENT')); - $inCart = $params['cart']->getProducts(); - - if (isset($params['orderStatus'])) { - $this->api->customersEdit( - array( - 'externalId' => $params['cart']->id_customer, - 'lastName' => $params['customer']->lastname, - 'firstName' => $params['customer']->firstname, - 'email' => $params['customer']->email, - 'phones' => array( - array( - 'number' => $address['phone'], - ), - array( - 'number' => $address['phone_mobile'], - ) - ), - 'createdAt' => $params['customer']->date_add - ) - ); - - $items = array(); - foreach ($inCart as $item) { - $items[] = array( - 'initialPrice' => (!empty($item['rate'])) ? $item['price'] + ($item['price'] * $item['rate'] / 100) : $item['price'], - 'quantity' => $item['quantity'], - 'productId' => $item['id_product'], - 'productName' => $item['name'], - 'createdAt' => $item['date_add'] - ); - } - - $dTypeKey = $params['cart']->id_carrier; - - if (Module::getInstanceByName('advancedcheckout') === false) { - $pTypeKey = $params['order']->module; - } else { - $pTypeKey = $params['order']->payment; - } - - $this->api->ordersCreate( - array( - 'externalId' => $params['order']->id, - 'orderType' => 'eshop-individual', - 'orderMethod' => 'shopping-cart', - 'status' => 'new', - 'customerId' => $params['cart']->id_customer, - 'firstName' => $params['customer']->firstname, - 'lastName' => $params['customer']->lastname, - 'phone' => $address['phone'], - 'email' => $params['customer']->email, - 'paymentType' => $payment->$pTypeKey, - 'delivery' => array( - 'code' => $delivery->$dTypeKey, - 'cost' => $params['order']->total_shipping, - 'address' => array( - 'city' => $address['city'], - 'index' => $address['postcode'], - 'text' => $address['address1'], - ) - ), - 'discount' => $params['order']->total_discounts, - 'items' => $items, - 'createdAt' => $params['order']->date_add - ) - ); - } - - if (!empty($params['newOrderStatus'])) { - $statuses = OrderState::getOrderStates($this->default_lang); - $aStatuses = json_decode(Configuration::get('RETAILCRM_API_STATUS')); - foreach ($statuses as $status) { - if ($status['name'] == $params['newOrderStatus']->name) { - $currStatus = $status['id_order_state']; - $this->api->ordersEdit( - array( - 'externalId' => $params['id_order'], - 'status' => $aStatuses->$currStatus - ) - ); - } - } - } - } -} From cc18170153ba9a06bd17f73c28206a2aa64b1c73 Mon Sep 17 00:00:00 2001 From: Alex Lushpai Date: Wed, 13 Jan 2016 01:49:36 +0300 Subject: [PATCH 15/17] partial fix status mapping, remove deprecated code --- retailcrm/lib/RetailcrmIcml.php | 2 +- retailcrm/lib/RetailcrmReferences.php | 11 +++- retailcrm/retailcrm.php | 80 +-------------------------- 3 files changed, 12 insertions(+), 81 deletions(-) diff --git a/retailcrm/lib/RetailcrmIcml.php b/retailcrm/lib/RetailcrmIcml.php index 5b5f950..825d9c2 100644 --- a/retailcrm/lib/RetailcrmIcml.php +++ b/retailcrm/lib/RetailcrmIcml.php @@ -117,7 +117,7 @@ class RetailcrmIcml $e->appendChild( $this->dd->createElement($key) )->appendChild( - $this->dd->createTextNode($offer[$key]) + $this->dd->createTextNode(trim($offer[$key])) ); } diff --git a/retailcrm/lib/RetailcrmReferences.php b/retailcrm/lib/RetailcrmReferences.php index 0f0d87d..1c63048 100644 --- a/retailcrm/lib/RetailcrmReferences.php +++ b/retailcrm/lib/RetailcrmReferences.php @@ -44,18 +44,25 @@ class RetailcrmReferences public function getStatuses() { $statusTypes = array(); + /** + * TODO: state ids duplicates between both arrays, temporary disable return states + * $states = array_merge( OrderState::getOrderStates($this->default_lang, true), OrderReturnState::getOrderReturnStates($this->default_lang, true) ); + */ + + $states = OrderState::getOrderStates($this->default_lang, true); if (!empty($states)) { foreach ($states as $state) { if ($state['name'] != ' ') { - $key = isset($state['id_order_state']) + /*$key = isset($state['id_order_state']) ? $state['id_order_state'] : $state['id_order_return_state'] - ; + ;*/ + $key = $state['id_order_state']; $statusTypes[] = array( 'type' => 'select', 'label' => $state['name'], diff --git a/retailcrm/retailcrm.php b/retailcrm/retailcrm.php index b2f2f08..e47cb4b 100644 --- a/retailcrm/retailcrm.php +++ b/retailcrm/retailcrm.php @@ -63,8 +63,7 @@ class RetailCRM extends Module Configuration::deleteByName('RETAILCRM_API_TOKEN') && Configuration::deleteByName('RETAILCRM_API_STATUS') && Configuration::deleteByName('RETAILCRM_API_DELIVERY') && - Configuration::deleteByName('RETAILCRM_LAST_SYNC') && - Configuration::deleteByName('RETAILCRM_API_ADDR'); + Configuration::deleteByName('RETAILCRM_LAST_SYNC'); } public function getContent() @@ -91,7 +90,6 @@ class RetailCRM extends Module $delivery = json_encode(Tools::getValue('RETAILCRM_API_DELIVERY')); $status = json_encode(Tools::getValue('RETAILCRM_API_STATUS')); $payment = json_encode(Tools::getValue('RETAILCRM_API_PAYMENT')); - $order_address = json_encode(Tools::getValue('RETAILCRM_API_ADDR')); if (!$address || empty($address) || !Validate::isGenericName($address)) { $output .= $this->displayError($this->l('Invalid crm address')); @@ -103,7 +101,6 @@ class RetailCRM extends Module Configuration::updateValue('RETAILCRM_API_DELIVERY', $delivery); Configuration::updateValue('RETAILCRM_API_STATUS', $status); Configuration::updateValue('RETAILCRM_API_PAYMENT', $payment); - Configuration::updateValue('RETAILCRM_API_ADDR', $order_address); $output .= $this->displayConfirmation($this->l('Settings updated')); } } @@ -176,14 +173,6 @@ class RetailCRM extends Module ); } - /* - * Address fields - */ -// $fields_form[4]['form'] = array( -// 'legend' => array('title' => $this->l('Address')), -// 'input' => $this->getAddressFields() -// ); - /* * Diplay forms */ @@ -256,67 +245,9 @@ class RetailCRM extends Module } } -// $addressSettings = Configuration::get('RETAILCRM_API_ADDR'); -// -// if (isset($addressSettings) && $addressSettings != '') { -// $addressTypes = json_decode($addressSettings); -// foreach ($addressTypes as $idx => $address) { -// $name = 'RETAILCRM_API_ADDR[' . $idx . ']'; -// $helper->fields_value[$name] = $address; -// } -// } - return $helper->generateForm($fields_form); } -// public function getAddressFields() -// { -// $addressFields = array(); -// $address = explode(' ', str_replace("\n", ' ', AddressFormat::getAddressCountryFormat($this->context->country->id))); -// -// if (!empty($address)) { -// foreach ($address as $idx => $a) { -// if (!in_array($a, array('vat_number', 'phone_mobile', 'company'))) { -// if (!strpos($a, ':')) { -// $a = preg_replace('/_/', ' ', $a); -// $a = preg_replace('/[\,\.]/', '', $a); -// $addressFields[] = array( -// 'type' => 'select', -// 'label' => $this->l((string)ucfirst($a)), -// 'name' => 'RETAILCRM_API_ADDR[' . $idx . ']', -// 'required' => false, -// 'options' => array( -// 'query' => array( -// array('name' => '', 'id_option' => ''), -// array('name' => $this->l('FIRST_NAME'), 'id_option' => 'first_name'), -// array('name' => $this->l('LAST_NAME'), 'id_option' => 'last_name'), -// array('name' => $this->l('PHONE'), 'id_option' => 'phone'), -// array('name' => $this->l('EMAIL'), 'id_option' => 'email'), -// array('name' => $this->l('ADDRESS'), 'id_option' => 'address'), -// array('name' => $this->l('COUNTRY'), 'id_option' => 'country'), -// array('name' => $this->l('REGION'), 'id_option' => 'region'), -// array('name' => $this->l('CITY'), 'id_option' => 'city'), -// array('name' => $this->l('ZIP'), 'id_option' => 'index'), -// array('name' => $this->l('STREET'), 'id_option' => 'street'), -// array('name' => $this->l('BUILDING'), 'id_option' => 'building'), -// array('name' => $this->l('FLAT'), 'id_option' => 'flat'), -// array('name' => $this->l('INTERCOMCODE'), 'id_option' => 'intercomcode'), -// array('name' => $this->l('FLOOR'), 'id_option' => 'floor'), -// array('name' => $this->l('BLOCK'), 'id_option' => 'block'), -// array('name' => $this->l('HOUSE'), 'ID' => 'house') -// ), -// 'id' => 'id_option', -// 'name' => 'name' -// ) -// ); -// } -// } -// } -// } -// -// return $addressFields; -// } - public function hookActionCustomerAccountAdd($params) { $this->api->customersCreate( @@ -365,14 +296,7 @@ class RetailCRM extends Module 'lastName' => $params['customer']->lastname, 'firstName' => $params['customer']->firstname, 'email' => $params['customer']->email, - 'phones' => array( - array( - 'number' => $address['phone'], - ), - array( - 'number' => $address['phone_mobile'], - ) - ), + 'phones' => array(array('number' => $address['phone'])), 'createdAt' => $params['customer']->date_add ) ); From 47a503fbb49d8ac155ad337c8932fe177971d50b Mon Sep 17 00:00:00 2001 From: Alex Lushpai Date: Wed, 13 Jan 2016 18:17:35 +0300 Subject: [PATCH 16/17] export archive script --- CHANGELOG.md | 19 ------ README.md | 48 +++++++++------ retailcrm/job/export.php | 124 +++++++++++++++++++++++++++------------ 3 files changed, 117 insertions(+), 74 deletions(-) delete mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 5cc9d14..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,19 +0,0 @@ -Changelog -========= - -v1.1 - -* Выгрузка клиентов при регистрации -* Учет наценок (НДС) при передаче данных по заказу в CRM -* Учет наценок (НДС) при генерации ICML - -v0.2 - -* Ребрендинг, intarocrm заменено на retailcrm -* Обновлена библиотека api-client-php -* Сборка через composer больше надоступна, все необходимые файлы включены в основноую поставку -* Добавлен механизм кастомизации -* Добавлены переводы -* Генерация ICML перенесена с контроллера на cron для более эффективного использования ресурсов -* Устранен раяд ошибок - diff --git a/README.md b/README.md index 37d7a8b..768f62b 100644 --- a/README.md +++ b/README.md @@ -9,32 +9,46 @@ Prestashop module * Синхронизировать справочники (способы доставки и оплаты, статусы заказов и т.п.) * Выгружать каталог товаров в формате [ICML](http://retailcrm.ru/docs/Разработчики/ФорматICML) (IntaroCRM Markup Language) -Установка -------------- +##Установка -### 1. Ручная установка +####Скачайте модуль +[Cкачать](http://download.retailcrm.pro/modules/prestashop/retailcrm-2.0.zip) -#### Скопируйте модуль -``` -git clone git@github.com:/intarocrm/prestashop-module.git -``` - -#### Создайте загружаемый .zip архив. -``` -cd prestashop-module -zip -r retailcrm.zip retailcrm -``` - -#### Установите через административный интерфейс управления модулями. +####Установите через административный интерфейс управления модулями. ![Установка модуля](/docs/images/add.png) -#### Перейдите к настройкам + +##Настройка + +####Перейдите к настройкам ![Настройка модуля](/docs/images/setup.png) -#### Введите адрес и API ключ вашей CRM и задайте соответствие справочников +####Введите адрес и API ключ вашей CRM и задайте соответствие справочников ![Справочники](/docs/images/ref.png) + +####Регулярная генерация выгрузки каталога + +Добавьте в крон запись вида + +``` +* */4 * * * /usr/bin/php /path/to/your/site/modules/retailcrm/job/icml.php +``` + +####Регулярное получение изменение из RetailCRM + +Добавьте в крон запись вида + +``` +*/7 * * * * /usr/bin/php /path/to/your/site/modules/retailcrm/job/sync.php +``` + +####Единоразовая выгрузка архива клиентов и заказов в RetailCRM + +``` +/usr/bin/php /path/to/your/site/modules/retailcrm/job/export.php +``` diff --git a/retailcrm/job/export.php b/retailcrm/job/export.php index 0426bf4..d2ca9d5 100644 --- a/retailcrm/job/export.php +++ b/retailcrm/job/export.php @@ -16,14 +16,29 @@ if (!empty($apiUrl) && !empty($apiKey)) { $orders = array(); $customers = array(); -$instance = new Order(); -$records = $instance->getOrdersWithInformations(2); -$delivery = json_decode(Configuration::get('RETAILCRM_API_DELIVERY')); -$payment = json_decode(Configuration::get('RETAILCRM_API_PAYMENT')); -$status = json_decode(Configuration::get('RETAILCRM_API_STATUS')); +$customerInstance = new Customer(); +$orderInstance = new Order(); -foreach ($records as $record) { +$customerRecords = $customerInstance->getCustomers(); +$orderRecords = $orderInstance->getOrdersWithInformations(); + +$delivery = json_decode(Configuration::get('RETAILCRM_API_DELIVERY'), true); +$payment = json_decode(Configuration::get('RETAILCRM_API_PAYMENT'), true); +$status = json_decode(Configuration::get('RETAILCRM_API_STATUS'), true); + +foreach ($customerRecords as $record) { + $customers[$record['id_customer']] = array( + 'externalId' => $record['id_customer'], + 'firstName' => $record['firstname'], + 'lastname' => $record['lastname'], + 'email' => $record['email'] + ); +} + +unset($customerRecords); + +foreach ($orderRecords as $record) { $object = new Order($record['id_order']); @@ -33,30 +48,67 @@ foreach ($records as $record) { $paymentType = $record['payment']; } + if ($record['current_state'] == 0) { + $order_status = 'completed'; + } else { + $order_status = array_key_exists($record['current_state'], $status) + ? $status[$record['current_state']] + : 'completed' + ; + } + $cart = new Cart($object->getCartIdStatic($record['id_order'])); $addressCollection = $cart->getAddressCollection(); $address = array_shift($addressCollection); + if ($address instanceof Address) { + $phone = is_null($address->phone) + ? is_null($address->phone_mobile) ? '' : $address->phone_mobile + : $address->phone + ; + + $postcode = $address->postcode; + $city = $address->city; + $addres_line = sprintf("%s %s", $address->address1, $address->address2); + } + $order = array( 'externalId' => $record['id_order'], 'createdAt' => $record['date_add'], - 'status' => $record['current_state'] == 0 ? 'new' : $status->$record['current_state'], + 'status' => $order_status, 'firstName' => $record['firstname'], 'lastName' => $record['lastname'], 'email' => $record['email'], - 'phone' => $address->phone, - 'delivery' => array( - 'code' => $delivery->$record['id_carrier'], - 'cost' => $record['total_shipping_tax_incl'], - 'address' => array( - 'index' => $address->postcode, - 'city' => $address->city, - 'street' => sprintf("%s %s", $address->address1, $address->address2) - ) - ), - 'paymentType' => $payment->$paymentType ); + if (isset($postcode)) { + $order['delivery']['address']['postcode'] = $postcode; + } + + if (isset($city)) { + $order['delivery']['address']['city'] = $city; + } + + if (isset($addres_line)) { + $order['delivery']['address']['text'] = $addres_line; + } + + if ($phone) { + $order['phone'] = $phone; + } + + if (array_key_exists($paymentType, $payment)) { + $order['paymentType'] = $payment[$paymentType]; + } + + if (array_key_exists($record['id_carrier'], $delivery)) { + $order['delivery']['code'] = $delivery[$record['id_carrier']]; + } + + if (isset($record['total_shipping_tax_incl']) && (int) $record['total_shipping_tax_incl'] > 0) { + $order['delivery']['cost'] = round($record['total_shipping_tax_incl'], 2); + } + $products = $object->getProducts(); foreach($products as $product) { @@ -73,27 +125,23 @@ foreach ($records as $record) { if ($record['id_customer']) { $order['customer']['externalId'] = $record['id_customer']; - - $customer = new Customer($record['id_customer']); - $customerCRM = array( - 'externalId' => $customer->id, - 'firstName' => $customer->firstname, - 'lastname' => $customer->lastname, - 'email' => $customer->email, - 'phones' => array(array('number' => $address->phone)), - 'createdAt' => $customer->date_add, - 'address' => array( - 'index' => $address->postcode, - 'city' => $address->city, - 'street' => sprintf("%s %s", $address->address1, $address->address2) - ) - ); - - $customers[$customer->id] = $customerCRM; } - $orders[] = $order; + $orders[$record['id_order']] = $order; } -var_dump(count($customers)); -var_dump(count($orders)); +unset($orderRecords); + +$customers = array_chunk($customers, 50); + +foreach ($customers as $chunk) { + $api->customersUpload($chunk); + time_nanosleep(0, 200000000); +} + +$orders = array_chunk($orders, 50); + +foreach ($orders as $chunk) { + $api->ordersUpload($chunk); + time_nanosleep(0, 200000000); +} From 77f302172ccda1314cc3b3e36ae797307bc1268e Mon Sep 17 00:00:00 2001 From: Alex Lushpai Date: Wed, 13 Jan 2016 18:19:51 +0300 Subject: [PATCH 17/17] readme update --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 768f62b..5ba61cb 100644 --- a/README.md +++ b/README.md @@ -9,29 +9,29 @@ Prestashop module * Синхронизировать справочники (способы доставки и оплаты, статусы заказов и т.п.) * Выгружать каталог товаров в формате [ICML](http://retailcrm.ru/docs/Разработчики/ФорматICML) (IntaroCRM Markup Language) -##Установка +###Установка -####Скачайте модуль +#####Скачайте модуль [Cкачать](http://download.retailcrm.pro/modules/prestashop/retailcrm-2.0.zip) -####Установите через административный интерфейс управления модулями. +#####Установите через административный интерфейс управления модулями. ![Установка модуля](/docs/images/add.png) -##Настройка +###Настройка -####Перейдите к настройкам +#####Перейдите к настройкам ![Настройка модуля](/docs/images/setup.png) -####Введите адрес и API ключ вашей CRM и задайте соответствие справочников +#####Введите адрес и API ключ вашей CRM и задайте соответствие справочников ![Справочники](/docs/images/ref.png) -####Регулярная генерация выгрузки каталога +#####Регулярная генерация выгрузки каталога Добавьте в крон запись вида @@ -39,7 +39,7 @@ Prestashop module * */4 * * * /usr/bin/php /path/to/your/site/modules/retailcrm/job/icml.php ``` -####Регулярное получение изменение из RetailCRM +#####Регулярное получение изменение из RetailCRM Добавьте в крон запись вида @@ -47,7 +47,7 @@ Prestashop module */7 * * * * /usr/bin/php /path/to/your/site/modules/retailcrm/job/sync.php ``` -####Единоразовая выгрузка архива клиентов и заказов в RetailCRM +#####Единоразовая выгрузка архива клиентов и заказов в RetailCRM ``` /usr/bin/php /path/to/your/site/modules/retailcrm/job/export.php