diff --git a/CHANGELOG.md b/CHANGELOG.md index a8edc66..d3170ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +## v2.4.0 +* Добавлена возможность выгрузки брошенных корзин + ## v2.3.4 * Добавлена поддержка передачи одинаковых товаров в заказе как разных товарных позиций diff --git a/VERSION b/VERSION index 3f684d2..197c4d5 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.3.4 +2.4.0 diff --git a/retailcrm/bootstrap.php b/retailcrm/bootstrap.php old mode 100644 new mode 100755 diff --git a/retailcrm/index.php b/retailcrm/index.php old mode 100644 new mode 100755 diff --git a/retailcrm/job/abandonedCarts.php b/retailcrm/job/abandonedCarts.php new file mode 100755 index 0000000..39c1a04 --- /dev/null +++ b/retailcrm/job/abandonedCarts.php @@ -0,0 +1,92 @@ + 0) { + $time = intval($time); +} else { + $time = 0; +} + +$now = new DateTime(); +$sql = 'SELECT c.id_cart, c.date_upd + FROM '._DB_PREFIX_.'cart AS c + WHERE id_customer != 0 + AND TIME_TO_SEC(TIMEDIFF(\''.pSQL($now->format('Y-m-d H:i:s')).'\', date_upd)) >= '.$time.' + AND c.id_cart NOT IN(SELECT id_cart from '._DB_PREFIX_.'orders);'; +$rows = Db::getInstance()->executeS($sql); +$status = Configuration::get('RETAILCRM_API_SYNCHRONIZED_CART_STATUS'); +$paymentTypes = array_keys(json_decode(Configuration::get('RETAILCRM_API_PAYMENT'), true)); + +if (empty($rows) + || empty($status) + || !$api + || (count($paymentTypes) < 1) +) { + return; +} + +foreach ($rows as $cartId) { + $cart = new Cart($cartId['id_cart']); + $cartExternalId = RetailCRM::getCartOrderExternalId($cart); + + $response = $api->ordersGet($cartExternalId); + + if ($response === false) { + $api->customersCreate(RetailCRM::buildCrmCustomer(new Customer($cart->id_customer))); + $order = RetailCRM::buildCrmOrderFromCart($cart, $cartExternalId, $paymentTypes[0], $status); + + if (empty($order)) { + continue; + } + + if ($api->ordersCreate($order) !== false) { + $cart->date_upd = date('Y-m-d H:i:s'); + $cart->save(); + } + + continue; + } + + if (isset($response['order']) && !empty($response['order'])) { + $order = RetailCRM::buildCrmOrderFromCart($cart, $response['order']['externalId'], $paymentTypes[0], $status); + + if (empty($order)) { + continue; + } + + if ($api->ordersEdit($order) !== false) { + $cart->date_upd = date('Y-m-d H:i:s'); + $cart->save(); + } + } +} diff --git a/retailcrm/job/export.php b/retailcrm/job/export.php old mode 100644 new mode 100755 diff --git a/retailcrm/job/icml.php b/retailcrm/job/icml.php old mode 100644 new mode 100755 diff --git a/retailcrm/job/index.php b/retailcrm/job/index.php old mode 100644 new mode 100755 diff --git a/retailcrm/job/inventories.php b/retailcrm/job/inventories.php old mode 100644 new mode 100755 diff --git a/retailcrm/job/jobs.php b/retailcrm/job/jobs.php new file mode 100755 index 0000000..589da88 --- /dev/null +++ b/retailcrm/job/jobs.php @@ -0,0 +1,18 @@ + null + ), + true +); diff --git a/retailcrm/job/missing.php b/retailcrm/job/missing.php old mode 100644 new mode 100755 diff --git a/retailcrm/job/sync.php b/retailcrm/job/sync.php old mode 100644 new mode 100755 diff --git a/retailcrm/lib/CurlException.php b/retailcrm/lib/CurlException.php old mode 100644 new mode 100755 diff --git a/retailcrm/lib/InvalidJsonException.php b/retailcrm/lib/InvalidJsonException.php old mode 100644 new mode 100755 diff --git a/retailcrm/lib/JobManager.php b/retailcrm/lib/JobManager.php new file mode 100755 index 0000000..4ec9165 --- /dev/null +++ b/retailcrm/lib/JobManager.php @@ -0,0 +1,444 @@ + DateInterval::createFromDateString('1 hour') + * ), + * true + * ); + * + * File `jobName.php` must exist in retailcrm/job and must contain everything to run job. + * Throwed errors will be logged in /retailcrm.log + * DateInterval must be positive. Pass `null` instead of DateInterval to remove + * any delay - in other words, jobs without interval will be executed every time. + * + * @param array $jobs + * @param bool $runOnceInContext + * + * @throws \Exception + */ + public static function startJobs($jobs = array(), $runOnceInContext = false) + { + $inBackground = static::canExecInBackground(); + + if ($inBackground) { + $cmdline = sprintf('%s "%s" %b', __FILE__, static::serializeJobs($jobs), $runOnceInContext); + static::execPHP($cmdline, true, $runOnceInContext); + } else { + static::execJobs($jobs, $runOnceInContext); + } + } + + /** + * Run scheduled jobs with request + * + * @param array $jobs + * @param bool $runOnceInContext + * + * @throws \Exception + */ + public static function execJobs($jobs = array(), $runOnceInContext = false) + { + $current = date_create('now'); + $lastRuns = static::getLastRuns(); + + if (!static::lock()) { + return; + } + + foreach ($jobs as $job => $diff) { + try { + if (isset($lastRuns[$job]) && $lastRuns[$job] instanceof DateTime) { + $shouldRunAt = clone $lastRuns[$job]; + } else { + $shouldRunAt = new DateTime(); + } + + if ($diff instanceof DateInterval) { + $shouldRunAt->add($diff); + } + + if (!isset($shouldRunAt) || $shouldRunAt <= $current) { + JobManager::runJob($job, $runOnceInContext); + $lastRuns[$job] = new DateTime(); + } + } catch (\Exception $exception) { + static::handleError($exception->getFile(), $exception->getMessage()); + } catch (\Throwable $throwable) { + static::handleError($throwable->getFile(), $throwable->getMessage()); + } + } + + static::setLastRuns($lastRuns); + static::unlock(); + } + + /** + * Extracts jobs last runs from db + * + * @return array + * @throws \Exception + */ + private static function getLastRuns() + { + $lastRuns = json_decode((string) Configuration::get(self::LAST_RUN_NAME), true); + + if (json_last_error() != JSON_ERROR_NONE) { + $lastRuns = []; + } else { + foreach ($lastRuns as $job => $ran) { + $lastRan = DateTime::createFromFormat(DATE_RFC3339, $ran); + + if ($lastRan instanceof DateTime) { + $lastRuns[$job] = $lastRan; + } else { + $lastRuns[$job] = new DateTime(); + } + } + } + + return (array) $lastRuns; + } + + /** + * Updates jobs last runs in db + * + * @param array $lastRuns + * + * @throws \Exception + */ + private static function setLastRuns($lastRuns = []) + { + $now = new DateTime(); + + if (!is_array($lastRuns)) { + $lastRuns = []; + } + + foreach ($lastRuns as $job => $ran) { + if ($ran instanceof DateTime) { + $lastRuns[$job] = $ran->format(DATE_RFC3339); + } else { + $lastRuns[$job] = $now->format(DATE_RFC3339); + } + } + + Configuration::updateValue(self::LAST_RUN_NAME, (string) json_encode($lastRuns)); + } + + /** + * Runs job + * + * @param string $job + * @param bool $once + */ + public static function runJob($job, $once = false) + { + $jobFile = implode(DIRECTORY_SEPARATOR, array(_PS_ROOT_DIR_, 'modules', 'retailcrm', 'job', $job . '.php')); + + if (!file_exists($jobFile)) { + throw new \InvalidArgumentException('Cannot find job'); + } + + static::execPHP($jobFile, false, $once); + } + + /** + * Runs PHP file + * + * @param $fileCommandLine + * @param bool $fork + * @param bool $once + */ + private static function execPHP($fileCommandLine, $fork = true, $once = false) + { + if ($fork) { + static::execInBackground(sprintf('%s %s', static::getPhpBinary(), $fileCommandLine)); + } else { + static::execHere($fileCommandLine, $once); + } + } + + /** + * Serializes jobs to JSON + * + * @param $jobs + * + * @return string + */ + public static function serializeJobs($jobs) + { + foreach ($jobs as $name => $interval) { + $jobs[$name] = serialize($interval); + } + + return (string) base64_encode(json_encode($jobs)); + } + + /** + * Unserializes jobs + * + * @param $jobsJson + * + * @return array + */ + public static function deserializeJobs($jobsJson) + { + $jobs = json_decode(base64_decode($jobsJson), true); + + if (json_last_error() != JSON_ERROR_NONE) { + throw new \InvalidArgumentException(sprintf('Invalid JSON: %s', json_last_error_msg())); + } + + if (!is_array($jobs) || count($jobs) == 0) { + throw new \InvalidArgumentException('Empty or invalid data'); + } + + foreach ($jobs as $name => $interval) { + if (!is_string($name) || !is_string($interval)) { + throw new \InvalidArgumentException('Invalid job in array'); + } + + $intervalObj = unserialize($interval); + + if (!($intervalObj instanceof DateInterval)) { + throw new \InvalidArgumentException('Invalid job interval in array'); + } + + $jobs[$name] = $intervalObj; + } + + return (array) $jobs; + } + + /** + * Writes error to log and returns 500 + * + * @param $file + * @param $msg + */ + private static function handleError($file, $msg) + { + error_log(sprintf('%s: %s', $file, $msg), 3, _PS_ROOT_DIR_ . '/retailcrm.log'); + http_response_code(500); + } + + /** + * Run process in background without waiting + * + * @param $cmd + * + * @return void + */ + private static function execInBackground($cmd) { + if (substr(php_uname(), 0, 7) == "Windows"){ + pclose(popen("start /B ". $cmd, "r")); + } else { + $outputPos = strpos($cmd, '>'); + + if ($outputPos !== false) { + $cmd = substr($cmd, 0, $outputPos); + } + + $command = $cmd . " > /dev/null &"; + + if (function_exists('exec')) { + exec($command); + } else if (function_exists('shell_exec')) { + shell_exec($command); + } else if (function_exists('passthru')) { + passthru($command); + } + } + } + + /** + * Executes php script in this context, without hanging up request + * + * @param string $phpScript + * @param bool $once + */ + private static function execHere($phpScript, $once = false) + { + ignore_user_abort( true); + set_time_limit(static::getTimeLimit()); + + if (version_compare(phpversion(), '7.0.16', '>=') && + function_exists('fastcgi_finish_request') + ) { + if (!headers_sent()) { + header('Expires: Thu, 19 Nov 1981 08:52:00 GMT'); + header('Cache-Control: no-store, no-cache, must-revalidate'); + } + + fastcgi_finish_request(); + } + + if ($once) { + require_once($phpScript); + } else { + require($phpScript); + } + } + + /** + * Returns true if system support execution in background + * + * @return bool + */ + private static function canExecInBackground() + { + if (substr(php_uname(), 0, 7) == "Windows"){ + return function_exists('pclose') && function_exists('popen'); + } else { + return function_exists('exec') + || function_exists('shell_exec') + || function_exists('passthru'); + } + } + + /** + * Returns path to current PHP binary + * + * @return string + */ + private static function getPhpBinary() + { + if (defined('PHP_BINARY') && !empty(PHP_BINARY)) { + return PHP_BINARY; + } + + if (defined('PHP_BINDIR') && !empty(PHP_BINDIR)) { + $version = phpversion(); + $filePath = implode(DIRECTORY_SEPARATOR, array(PHP_BINDIR, 'php' . $version)); + + while (strlen($version) != 0 && !file_exists($filePath)) { + $dotPos = strrpos($version, '.'); + + if ($dotPos !== false) { + $version = substr($version, 0, strrpos($version, '.')); + } else { + $version = ''; + } + + $filePath = implode(DIRECTORY_SEPARATOR, array(PHP_BINDIR, 'php' . $version)); + } + + if (file_exists($filePath)) { + return $filePath; + } + } + + return 'php'; + } + + /** + * Returns script execution time limit + * + * @return int + */ + private static function getTimeLimit() + { + return 14400; + } + + /** + * Returns true if lock is present and it's not expired + * + * @return bool + */ + private static function isLocked() + { + $inProcess = (bool) Configuration::get(self::IN_PROGRESS_NAME); + $lastRan = date_create_from_format(DATE_RFC3339, (string) Configuration::get(self::LAST_RUN_NAME)); + $lastRanSeconds = $lastRan instanceof DateTime ? $lastRan->format('U') : time(); + + if (($lastRanSeconds + self::getTimeLimit()) < time()) { + static::unlock(); + + return false; + } + + return $inProcess; + } + + /** + * Installs lock + * + * @return bool + */ + private static function lock() + { + if (!static::isLocked()) { + Configuration::updateValue(self::IN_PROGRESS_NAME, true); + + return true; + } + + return false; + } + + /** + * Removes lock + * + * @return bool + */ + private static function unlock() + { + Configuration::updateValue(self::IN_PROGRESS_NAME, false); + + return false; + } +} + +if (PHP_SAPI == 'cli' && $argc == 3) { + try { + $jobs = JobManager::deserializeJobs($argv[1]); + $runOnce = (bool) $argv[2]; + } catch (InvalidArgumentException $exception) { + printf('Error: %s%s', $exception->getMessage(), PHP_EOL); + exit(0); + } + + JobManager::execJobs($jobs, $runOnce); +} \ No newline at end of file diff --git a/retailcrm/lib/RetailcrmApiClientV4.php b/retailcrm/lib/RetailcrmApiClientV4.php old mode 100644 new mode 100755 diff --git a/retailcrm/lib/RetailcrmApiClientV5.php b/retailcrm/lib/RetailcrmApiClientV5.php old mode 100644 new mode 100755 diff --git a/retailcrm/lib/RetailcrmApiErrors.php b/retailcrm/lib/RetailcrmApiErrors.php old mode 100644 new mode 100755 diff --git a/retailcrm/lib/RetailcrmApiResponse.php b/retailcrm/lib/RetailcrmApiResponse.php old mode 100644 new mode 100755 diff --git a/retailcrm/lib/RetailcrmCatalog.php b/retailcrm/lib/RetailcrmCatalog.php old mode 100644 new mode 100755 diff --git a/retailcrm/lib/RetailcrmDaemonCollector.php b/retailcrm/lib/RetailcrmDaemonCollector.php old mode 100644 new mode 100755 diff --git a/retailcrm/lib/RetailcrmHistory.php b/retailcrm/lib/RetailcrmHistory.php index 764388a..7aabe0c 100755 --- a/retailcrm/lib/RetailcrmHistory.php +++ b/retailcrm/lib/RetailcrmHistory.php @@ -173,6 +173,7 @@ class RetailcrmHistory $sinceId = $end['id']; $statuses = array_flip(array_filter(json_decode(Configuration::get('RETAILCRM_API_STATUS'), true))); + $cartStatus = (string)(Configuration::get('RETAILCRM_API_SYNCHRONIZED_CART_STATUS')); $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))); $deliveryDefault = json_decode(Configuration::get('RETAILCRM_API_DELIVERY_DEFAULT'), true); @@ -189,6 +190,10 @@ class RetailcrmHistory if ($responce) { $order = $responce['order']; + + if ($order['status'] == $cartStatus) { + continue; + } } else { continue; } diff --git a/retailcrm/lib/RetailcrmHistoryHelper.php b/retailcrm/lib/RetailcrmHistoryHelper.php old mode 100644 new mode 100755 diff --git a/retailcrm/lib/RetailcrmHttpClient.php b/retailcrm/lib/RetailcrmHttpClient.php old mode 100644 new mode 100755 diff --git a/retailcrm/lib/RetailcrmIcml.php b/retailcrm/lib/RetailcrmIcml.php old mode 100644 new mode 100755 diff --git a/retailcrm/lib/RetailcrmInventories.php b/retailcrm/lib/RetailcrmInventories.php old mode 100644 new mode 100755 diff --git a/retailcrm/lib/RetailcrmProxy.php b/retailcrm/lib/RetailcrmProxy.php old mode 100644 new mode 100755 diff --git a/retailcrm/lib/RetailcrmReferences.php b/retailcrm/lib/RetailcrmReferences.php old mode 100644 new mode 100755 diff --git a/retailcrm/lib/RetailcrmService.php b/retailcrm/lib/RetailcrmService.php old mode 100644 new mode 100755 diff --git a/retailcrm/lib/index.php b/retailcrm/lib/index.php old mode 100644 new mode 100755 diff --git a/retailcrm/logo.gif b/retailcrm/logo.gif old mode 100644 new mode 100755 diff --git a/retailcrm/logo.png b/retailcrm/logo.png old mode 100644 new mode 100755 diff --git a/retailcrm/objects.xml b/retailcrm/objects.xml old mode 100644 new mode 100755 diff --git a/retailcrm/public/css/.gitignore b/retailcrm/public/css/.gitignore new file mode 100644 index 0000000..6be0402 --- /dev/null +++ b/retailcrm/public/css/.gitignore @@ -0,0 +1 @@ +*.min.css \ No newline at end of file diff --git a/retailcrm/public/css/retailcrm-upload.css b/retailcrm/public/css/retailcrm-upload.css old mode 100644 new mode 100755 diff --git a/retailcrm/public/js/.gitignore b/retailcrm/public/js/.gitignore new file mode 100644 index 0000000..0baf809 --- /dev/null +++ b/retailcrm/public/js/.gitignore @@ -0,0 +1 @@ +*.min.js \ No newline at end of file diff --git a/retailcrm/public/js/exec-jobs.js b/retailcrm/public/js/exec-jobs.js new file mode 100755 index 0000000..cd2fc8d --- /dev/null +++ b/retailcrm/public/js/exec-jobs.js @@ -0,0 +1,6 @@ +(function () { + var xhr = new XMLHttpRequest(); + xhr.open('GET', '/modules/retailcrm/job/jobs.php', true); + xhr.timeout = 0; + xhr.send(null); +})(); \ No newline at end of file diff --git a/retailcrm/public/js/retailcrm-upload.js b/retailcrm/public/js/retailcrm-upload.js old mode 100644 new mode 100755 diff --git a/retailcrm/retailcrm.php b/retailcrm/retailcrm.php index b5758d5..6a74b3e 100755 --- a/retailcrm/retailcrm.php +++ b/retailcrm/retailcrm.php @@ -31,6 +31,7 @@ class RetailCRM extends Module public $log; public $confirmUninstall; public $reference; + public $assetsBase; private $use_new_hooks = true; @@ -38,7 +39,7 @@ class RetailCRM extends Module { $this->name = 'retailcrm'; $this->tab = 'export'; - $this->version = '2.3.4'; + $this->version = '2.4.0'; $this->author = 'Retail Driver LCC'; $this->displayName = $this->l('RetailCRM'); $this->description = $this->l('Integration module for RetailCRM'); @@ -53,6 +54,12 @@ class RetailCRM extends Module $this->psVersion = Tools::substr(_PS_VERSION_, 0, 3); $this->log = _PS_ROOT_DIR_ . '/retailcrm.log'; $this->module_key = '149c765c6cddcf35e1f13ea6c71e9fa5'; + $this->assetsBase = + Tools::getShopDomainSsl(true, true) . + __PS_BASE_URI__ . + 'modules/' . + $this->name . + '/public'; if ($this->psVersion == '1.6') { $this->bootstrap = true; @@ -84,6 +91,8 @@ class RetailCRM extends Module public function hookHeader() { + $this->context->controller->addJS($this->assetsBase . '/js/exec-jobs.js'); + if (Configuration::get('RETAILCRM_DAEMON_COLLECTOR_ACTIVE') && Configuration::get('RETAILCRM_DAEMON_COLLECTOR_KEY') ) { @@ -116,6 +125,9 @@ class RetailCRM extends Module Configuration::deleteByName('RETAILCRM_LAST_SYNC') && Configuration::deleteByName('RETAILCRM_API_VERSION') && Configuration::deleteByName('RETAILCRM_LAST_CUSTOMERS_SYNC') && + Configuration::deleteByName('RETAILCRM_API_SYNCHRONIZE_CARTS') && + Configuration::deleteByName('RETAILCRM_API_SYNCHRONIZED_CART_STATUS') && + Configuration::deleteByName('RETAILCRM_API_SYNCHRONIZED_CART_DELAY') && Configuration::deleteByName('RETAILCRM_LAST_ORDERS_SYNC'); } @@ -144,12 +156,18 @@ class RetailCRM extends Module $collectorActive = (Tools::getValue('RETAILCRM_DAEMON_COLLECTOR_ACTIVE_1')); $collectorKey = (string)(Tools::getValue('RETAILCRM_DAEMON_COLLECTOR_KEY')); $clientId = Configuration::get('RETAILCRM_CLIENT_ID'); + $synchronizeCartsActive = (Tools::getValue('RETAILCRM_API_SYNCHRONIZE_CARTS_1')); + $synchronizedCartStatus = (string)(Tools::getValue('RETAILCRM_API_SYNCHRONIZED_CART_STATUS')); + $synchronizedCartDelay = (string)(Tools::getValue('RETAILCRM_API_SYNCHRONIZED_CART_DELAY')); $settings = array( 'address' => $address, 'token' => $token, 'version' => $version, - 'clientId' => $clientId + 'clientId' => $clientId, + 'status' => $status, + 'statusExport' => $statusExport, + 'synchronizeCartStatus' => $synchronizedCartStatus ); $output .= $this->validateForm($settings, $output); @@ -166,6 +184,9 @@ class RetailCRM extends Module Configuration::updateValue('RETAILCRM_STATUS_EXPORT', $statusExport); Configuration::updateValue('RETAILCRM_DAEMON_COLLECTOR_ACTIVE', $collectorActive); Configuration::updateValue('RETAILCRM_DAEMON_COLLECTOR_KEY', $collectorKey); + Configuration::updateValue('RETAILCRM_API_SYNCHRONIZE_CARTS', $synchronizeCartsActive); + Configuration::updateValue('RETAILCRM_API_SYNCHRONIZED_CART_STATUS', $synchronizedCartStatus); + Configuration::updateValue('RETAILCRM_API_SYNCHRONIZED_CART_DELAY', $synchronizedCartDelay); $output .= $this->displayConfirmation($this->l('Settings updated')); } @@ -189,14 +210,9 @@ class RetailCRM extends Module "$address/admin/settings#t-main" ); - $assetsBase = - Tools::getShopDomainSsl(true, true) . - __PS_BASE_URI__ . - 'modules/' . - $this->name . - '/public'; - $this->context->controller->addCSS($assetsBase . '/css/retailcrm-upload.css'); - $this->context->controller->addJS($assetsBase . '/js/retailcrm-upload.js'); + $this->context->controller->addCSS($this->assetsBase . '/css/retailcrm-upload.css'); + $this->context->controller->addJS($this->assetsBase . '/js/retailcrm-upload.js'); + $this->context->controller->addJS($this->assetsBase . '/js/exec-jobs.js'); $this->display(__FILE__, 'retailcrm.tpl'); return $output . $this->displaySettingsForm() . $this->displayUploadOrdersForm(); @@ -278,7 +294,45 @@ class RetailCRM extends Module */ public static function verifyDate($date, $format = "Y-m-d") { - return (bool)date_create_from_format($format, $date); + return $date !== "0000-00-00" && (bool)date_create_from_format($format, $date); + } + + /** + * Build array with order data for retailCRM from PrestaShop cart data + * + * @param Cart $cart Cart with data + * @param string $externalId External ID for order + * @param string $paymentType Payment type (buildCrmOrder requires it) + * @param string $status Status for order + * + * @return array + */ + public static function buildCrmOrderFromCart(Cart $cart = null, $externalId = '', $paymentType = '', $status = '') + { + if (empty($cart) || empty($paymentType) || empty($status)) { + return array(); + } + + $order = new Order(); + $order->id_cart = $cart->id; + $order->id_customer = $cart->id_customer; + $order->total_discounts = 0; + $order->module = $paymentType; + $order->payment = $paymentType; + $orderData = static::buildCrmOrder( + $order, + new Customer($cart->id_customer), + $cart, + false, + true, + true + ); + $orderData['externalId'] = $externalId; + $orderData['status'] = $status; + + unset($orderData['payments']); + + return $orderData; } /** @@ -289,6 +343,7 @@ class RetailCRM extends Module * @param Cart $orderCart Cart for provided order. Optional * @param bool $isStatusExport Use status for export * @param bool $preferCustomerAddress Use customer address even if delivery address is provided + * @param bool $dataFromCart Prefer data from cart * * @return array retailCRM order data */ @@ -297,7 +352,8 @@ class RetailCRM extends Module Customer $customer = null, Cart $orderCart = null, $isStatusExport = false, - $preferCustomerAddress = false + $preferCustomerAddress = false, + $dataFromCart = false ) { $apiVersion = Configuration::get('RETAILCRM_API_VERSION'); $statusExport = Configuration::get('RETAILCRM_STATUS_EXPORT'); @@ -357,6 +413,10 @@ class RetailCRM extends Module return $v->id_customer == $customer->id; } ); + + if (is_array($address) && count($address) == 1) { + $address = reset($address); + } } $address = static::addressParse($address); @@ -388,16 +448,34 @@ class RetailCRM extends Module $crmOrder['payments'] = array(); } - if (array_key_exists($order->id_carrier, $delivery) && !empty($delivery[$order->id_carrier])) { - $crmOrder['delivery']['code'] = $delivery[$order->id_carrier]; + $idCarrier = $dataFromCart ? $cart->id_carrier : $order->id_carrier; + + if (empty($idCarrier)) { + $idCarrier = $order->id_carrier; + $totalShipping = $order->total_shipping; + $totalShippingWithoutTax = $order->total_shipping_tax_excl; + } else { + $totalShipping = $dataFromCart ? $cart->getCarrierCost($idCarrier) : $order->total_shipping; + + if (!empty($totalShipping) && $totalShipping != 0) { + $totalShippingWithoutTax = $dataFromCart + ? $totalShipping - $cart->getCarrierCost($idCarrier, false) + : $order->total_shipping_tax_excl; + } else { + $totalShippingWithoutTax = $order->total_shipping_tax_excl; + } } - if (isset($order->total_shipping) && ((int) $order->total_shipping) > 0) { - $crmOrder['delivery']['cost'] = round($order->total_shipping, 2); + if (array_key_exists($idCarrier, $delivery) && !empty($delivery[$idCarrier])) { + $crmOrder['delivery']['code'] = $delivery[$idCarrier]; } - if (isset($order->total_shipping_tax_excl) && $order->total_shipping_tax_excl > 0) { - $crmOrder['delivery']['netCost'] = round($order->total_shipping_tax_excl, 2); + if (isset($totalShipping) && ((int) $totalShipping) > 0) { + $crmOrder['delivery']['cost'] = round($totalShipping, 2); + } + + if (isset($totalShippingWithoutTax) && $totalShippingWithoutTax > 0) { + $crmOrder['delivery']['netCost'] = round($totalShippingWithoutTax, 2); } $comment = $order->getFirstMessage(); @@ -406,7 +484,30 @@ class RetailCRM extends Module $crmOrder['customerComment'] = $comment; } - foreach ($order->getProducts() as $product) { + if ($dataFromCart) { + $productStore = $cart; + $converter = function ($product) { + $product['product_attribute_id'] = $product['id_product_attribute']; + $product['product_quantity'] = $product['cart_quantity']; + $product['product_id'] = $product['id_product']; + $product['id_order_detail'] = $product['id_product']; + $product['product_name'] = $product['name']; + $product['product_price'] = $product['price']; + $product['purchase_supplier_price'] = $product['price']; + $product['product_price_wt'] = $product['price_wt']; + + return $product; + }; + } else { + $productStore = $order; + $converter = function ($product) { + return $product; + }; + } + + foreach ($productStore->getProducts() as $productData) { + $product = $converter($productData); + if (isset($product['product_attribute_id']) && $product['product_attribute_id'] > 0) { $productId = $product['product_id'] . '#' . $product['product_attribute_id']; } else { @@ -605,6 +706,84 @@ class RetailCRM extends Module ); if ($this->api) { + /* + * Synchronize carts form + */ + $fields_form[]['form'] = array( + 'legend' => array( + 'title' => $this->l('Synchronization of buyer carts'), + ), + 'input' => array( + array( + 'type' => 'checkbox', + 'label' => $this->l('Create orders for abandoned carts of buyers'), + 'name' => 'RETAILCRM_API_SYNCHRONIZE_CARTS', + 'values' => array( + 'query' => array( + array( + 'id_option' => 1, + ) + ), + 'id' => 'id_option', + 'name' => 'name' + ) + ), + array( + 'type' => 'select', + 'name' => 'RETAILCRM_API_SYNCHRONIZED_CART_STATUS', + 'label' => $this->l('Order status for abandoned carts of buyers'), + 'options' => array( + 'query' => $this->reference->getStatuseDefaultExport(), + 'id' => 'id_option', + 'name' => 'name' + ) + ), + array( + 'type' => 'select', + 'name' => 'RETAILCRM_API_SYNCHRONIZED_CART_DELAY', + 'label' => $this->l('Upload abandoned carts'), + 'options' => array( + 'query' => array( + array( + 'id_option' => '0', + 'name' => $this->l('Immediately') + ), + array( + 'id_option' => '60', + 'name' => $this->l('After 1 minute') + ), + array( + 'id_option' => '300', + 'name' => $this->l('After 5 minutes') + ), + array( + 'id_option' => '600', + 'name' => $this->l('After 10 minutes') + ), + array( + 'id_option' => '900', + 'name' => $this->l('After 15 minutes') + ), + array( + 'id_option' => '1800', + 'name' => $this->l('After 30 minutes') + ), + array( + 'id_option' => '2700', + 'name' => $this->l('After 45 minute') + ), + array( + 'id_option' => '3600', + 'name' => $this->l('After 1 hour') + ), + ), + 'id' => 'id_option', + 'name' => 'name' + ) + ) + ) + ); + /* * Delivery */ @@ -698,6 +877,9 @@ class RetailCRM extends Module $helper->fields_value['RETAILCRM_API_VERSION'] = Configuration::get('RETAILCRM_API_VERSION'); $helper->fields_value['RETAILCRM_STATUS_EXPORT'] = Configuration::get('RETAILCRM_STATUS_EXPORT'); $helper->fields_value['RETAILCRM_DAEMON_COLLECTOR_ACTIVE_1'] = Configuration::get('RETAILCRM_DAEMON_COLLECTOR_ACTIVE'); + $helper->fields_value['RETAILCRM_API_SYNCHRONIZE_CARTS_1'] = Configuration::get('RETAILCRM_API_SYNCHRONIZE_CARTS'); + $helper->fields_value['RETAILCRM_API_SYNCHRONIZED_CART_STATUS'] = Configuration::get('RETAILCRM_API_SYNCHRONIZED_CART_STATUS'); + $helper->fields_value['RETAILCRM_API_SYNCHRONIZED_CART_DELAY'] = Configuration::get('RETAILCRM_API_SYNCHRONIZED_CART_DELAY'); $helper->fields_value['RETAILCRM_DAEMON_COLLECTOR_KEY'] = Configuration::get('RETAILCRM_DAEMON_COLLECTOR_KEY'); $deliverySettings = Configuration::get('RETAILCRM_API_DELIVERY'); @@ -751,6 +933,24 @@ class RetailCRM extends Module } } + $synchronizedCartsStatusDefault = Configuration::get('RETAILCRM_API_SYNCHRONIZED_CART_STATUS'); + if (isset($synchronizedCartsStatusDefault) && $synchronizedCartsStatusDefault != '') { + $synchronizedCartsStatus = json_decode($synchronizedCartsStatusDefault); + if ($synchronizedCartsStatus) { + $name = 'RETAILCRM_API_SYNCHRONIZED_CART_STATUS'; + $helper->fields_value[$name] = $synchronizedCartsStatus; + } + } + + $synchronizedCartsDelayDefault = Configuration::get('RETAILCRM_API_SYNCHRONIZED_CART_DELAY'); + if (isset($synchronizedCartsDelayDefault) && $synchronizedCartsDelayDefault != '') { + $synchronizedCartsDelay = json_decode($synchronizedCartsDelayDefault); + if ($synchronizedCartsDelay) { + $name = 'RETAILCRM_API_SYNCHRONIZED_CART_DELAY'; + $helper->fields_value[$name] = $synchronizedCartsDelay; + } + } + return $helper->generateForm($fields_form); } @@ -1036,9 +1236,15 @@ class RetailCRM extends Module if (isset($params['orderStatus'])) { $cart = $params['cart']; - $order = static::buildCrmOrder($params['order'], $params['customer'], $params['cart'], false); + $response = $this->api->ordersGet(self::getCartOrderExternalId($cart)); + $order = static::buildCrmOrder($params['order'], $params['customer'], $cart, false); - $this->api->ordersCreate($order); + if (!empty($response) && isset($response['order'])) { + $order['id'] = $response['order']['id']; + $this->api->ordersEdit($order, 'id'); + } else { + $this->api->ordersCreate($order); + } return true; @@ -1159,6 +1365,15 @@ class RetailCRM extends Module return false; } + private function validateStatuses($statuses, $statusExport, $cartStatus) + { + if ($cartStatus != '' && ($cartStatus == $statusExport || (stripos($statuses, $cartStatus) !== false))) { + return false; + } + + return true; + } + private function validateForm($settings, $output) { if (!$this->validateCrmAddress($settings['address']) || !Validate::isGenericName($settings['address'])) { @@ -1167,11 +1382,29 @@ class RetailCRM extends Module $output .= $this->displayError($this->l('Invalid or empty crm api token')); } elseif (!$this->validateApiVersion($settings)) { $output .= $this->displayError($this->l('The selected version of the API is unavailable')); + } elseif (!$this->validateStatuses( + $settings['status'], + $settings['statusExport'], + $settings['synchronizeCartStatus']) + ) { + $output .= $this->displayError($this->l('Order status for abandoned carts should not be used in other settings')); } return $output; } + /** + * Returns externalId for order + * + * @param Cart $cart + * + * @return string + */ + public static function getCartOrderExternalId(Cart $cart) + { + return sprintf('pscart_%d', $cart->id); + } + /** * Activate/deactivate module in marketplace retailCRM * diff --git a/retailcrm/translations/es.php b/retailcrm/translations/es.php old mode 100644 new mode 100755 index 6db2eca..f7b3285 --- a/retailcrm/translations/es.php +++ b/retailcrm/translations/es.php @@ -61,4 +61,17 @@ $_MODULE['<{retailcrm}prestashop>retailcrm_acfa058ec9e6e4745eddc0cae3f0f881'] = $_MODULE['<{retailcrm}prestashop>retailcrm_91412465ea9169dfd901dd5e7c96dd99'] = 'Exportar'; $_MODULE['<{retailcrm}prestashop>retailcrm_6bd461d1fc51b3294c6513cecc24758d'] = 'Los pedidos han sido cargados con éxito'; $_MODULE['<{retailcrm}prestashop>retailcrm_3518f7b8d79f91da4c91772b4c46db94'] = 'No se han podido cargar algunos pedidos'; -$_MODULE['<{retailcrm}prestashop>retailcrm_9a7fc06b4b2359f1f26f75fbbe27a3e8'] = 'No todos los pedidos se han cargado con existo'; \ No newline at end of file +$_MODULE['<{retailcrm}prestashop>retailcrm_9a7fc06b4b2359f1f26f75fbbe27a3e8'] = 'No todos los pedidos se han cargado con existo'; +$_MODULE['<{retailcrm}prestashop>retailcrm_917afe348e09163269225a89a825e634'] = 'Sincronización de carritos de compradores'; +$_MODULE['<{retailcrm}prestashop>retailcrm_d8e002d770b6f98af7b7ae9a0e5acfe9'] = 'Crear pedidos para carritos abandonados de compradores'; +$_MODULE['<{retailcrm}prestashop>retailcrm_35b5a9139a54caeb925556ceb2c38086'] = 'Estado del pedido para carritos abandonados de compradores'; +$_MODULE['<{retailcrm}prestashop>retailcrm_b9c4e8fe56eabcc4c7913ebb2f8eb388'] = 'Estado del pedido para carritos abandonados no debe ser utilizado en otros ajustes'; +$_MODULE['<{retailcrm}prestashop>retailcrm_a0d135501a738c3c98de385dc28cda61'] = 'Cargar carritos abandonados'; +$_MODULE['<{retailcrm}prestashop>retailcrm_fd83e0ccb3e6312a62f888dd496dd0a5'] = 'Instantáneamente'; +$_MODULE['<{retailcrm}prestashop>retailcrm_6c70ac5cc9fe8dff4f3bfe765dfe8798'] = 'Tras 1 minuto'; +$_MODULE['<{retailcrm}prestashop>retailcrm_be76333ef50b23b6337d263fc87fe11a'] = 'Tras 5 minutos'; +$_MODULE['<{retailcrm}prestashop>retailcrm_6e74495fad332de9a8207b5416de3cce'] = 'Tras 10 minutos'; +$_MODULE['<{retailcrm}prestashop>retailcrm_d5bb7c2cb1565fb1568924b01847b330'] = 'Tras 15 minutos'; +$_MODULE['<{retailcrm}prestashop>retailcrm_9d3095e54f694bb41ef4a3e62ed90e7a'] = 'Tras 30 minutos'; +$_MODULE['<{retailcrm}prestashop>retailcrm_dfb403fd86851c7d9f97706dff5a2327'] = 'Tras 45 minutos'; +$_MODULE['<{retailcrm}prestashop>retailcrm_4b5e6470d5d85448fcd89c828352d25e'] = 'Tras 1 hora'; \ No newline at end of file diff --git a/retailcrm/translations/index.php b/retailcrm/translations/index.php old mode 100644 new mode 100755 diff --git a/retailcrm/translations/ru.php b/retailcrm/translations/ru.php old mode 100644 new mode 100755 index 1f0def2..2e7be52 --- a/retailcrm/translations/ru.php +++ b/retailcrm/translations/ru.php @@ -61,4 +61,17 @@ $_MODULE['<{retailcrm}prestashop>retailcrm_acfa058ec9e6e4745eddc0cae3f0f881'] = $_MODULE['<{retailcrm}prestashop>retailcrm_91412465ea9169dfd901dd5e7c96dd99'] = 'Выгрузить'; $_MODULE['<{retailcrm}prestashop>retailcrm_6bd461d1fc51b3294c6513cecc24758d'] = 'Все заказы успешно загружены'; $_MODULE['<{retailcrm}prestashop>retailcrm_3518f7b8d79f91da4c91772b4c46db94'] = 'Некоторые заказы не удалось загрузить'; -$_MODULE['<{retailcrm}prestashop>retailcrm_9a7fc06b4b2359f1f26f75fbbe27a3e8'] = 'Не все заказы загружены успешно'; \ No newline at end of file +$_MODULE['<{retailcrm}prestashop>retailcrm_9a7fc06b4b2359f1f26f75fbbe27a3e8'] = 'Не все заказы загружены успешно'; +$_MODULE['<{retailcrm}prestashop>retailcrm_917afe348e09163269225a89a825e634'] = 'Синхронизация корзин покупателей'; +$_MODULE['<{retailcrm}prestashop>retailcrm_d8e002d770b6f98af7b7ae9a0e5acfe9'] = 'Создавать заказы для брошенных корзин покупателей'; +$_MODULE['<{retailcrm}prestashop>retailcrm_35b5a9139a54caeb925556ceb2c38086'] = 'Статус заказа для брошенных корзин покупателей'; +$_MODULE['<{retailcrm}prestashop>retailcrm_b9c4e8fe56eabcc4c7913ebb2f8eb388'] = 'Статус заказа для брошенных корзин не должен использоваться в других настройках'; +$_MODULE['<{retailcrm}prestashop>retailcrm_a0d135501a738c3c98de385dc28cda61'] = 'Выгружать брошенные корзины'; +$_MODULE['<{retailcrm}prestashop>retailcrm_fd83e0ccb3e6312a62f888dd496dd0a5'] = 'Мгновенно'; +$_MODULE['<{retailcrm}prestashop>retailcrm_6c70ac5cc9fe8dff4f3bfe765dfe8798'] = 'Через 1 минуту'; +$_MODULE['<{retailcrm}prestashop>retailcrm_be76333ef50b23b6337d263fc87fe11a'] = 'Через 5 минут'; +$_MODULE['<{retailcrm}prestashop>retailcrm_6e74495fad332de9a8207b5416de3cce'] = 'Через 10 минут'; +$_MODULE['<{retailcrm}prestashop>retailcrm_d5bb7c2cb1565fb1568924b01847b330'] = 'Через 15 минут'; +$_MODULE['<{retailcrm}prestashop>retailcrm_9d3095e54f694bb41ef4a3e62ed90e7a'] = 'Через 30 минут'; +$_MODULE['<{retailcrm}prestashop>retailcrm_dfb403fd86851c7d9f97706dff5a2327'] = 'Через 45 минут'; +$_MODULE['<{retailcrm}prestashop>retailcrm_4b5e6470d5d85448fcd89c828352d25e'] = 'Через 1 час'; \ No newline at end of file