PageRenderTime 59ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 1ms

/avalaratax/avalaratax.php

https://github.com/boxdrop/PrestaShop-modules
PHP | 1492 lines | 1255 code | 123 blank | 114 comment | 200 complexity | da98ca1894b4295f70062a28c46107b2 MD5 | raw file
Possible License(s): CC-BY-SA-3.0
  1. <?php
  2. /*
  3. * 2007-2011 PrestaShop
  4. *
  5. * NOTICE OF LICENSE
  6. *
  7. * This source file is subject to the Academic Free License (AFL 3.0)
  8. * that is bundled with this package in the file LICENSE.txt.
  9. * It is also available through the world-wide-web at this URL:
  10. * http://opensource.org/licenses/afl-3.0.php
  11. * If you did not receive a copy of the license and are unable to
  12. * obtain it through the world-wide-web, please send an email
  13. * to license@prestashop.com so we can send you a copy immediately.
  14. *
  15. * DISCLAIMER
  16. *
  17. * Do not edit or add to this file if you wish to upgrade PrestaShop to newer
  18. * versions in the future. If you wish to customize PrestaShop for your
  19. * needs please refer to http://www.prestashop.com for more information.
  20. *
  21. * @author PrestaShop SA <contact@prestashop.com>
  22. * @copyright 2007-2014 PrestaShop SA
  23. * @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
  24. * International Registered Trademark & Property of PrestaShop SA
  25. */
  26. // Security
  27. if (!defined('_PS_VERSION_'))
  28. exit;
  29. spl_autoload_register('avalaraAutoload');
  30. class AvalaraTax extends Module
  31. {
  32. /**
  33. * @brief Constructor
  34. */
  35. public function __construct()
  36. {
  37. $this->name = 'avalaratax';
  38. $this->tab = 'billing_invoicing';
  39. $this->version = '3.5';
  40. $this->author = 'PrestaShop';
  41. parent::__construct();
  42. $this->displayName = $this->l('Avalara - AvaTax');
  43. $this->description = $this->l('Sales Tax is complicated. AvaTax makes it easy.');
  44. /** Backward compatibility */
  45. require(_PS_MODULE_DIR_.$this->name.'/backward_compatibility/backward.php');
  46. if (!extension_loaded('soap') || !class_exists('SoapClient'))
  47. $this->warning = $this->l('SOAP extension should be enabled on your server to use this module.');
  48. }
  49. /**
  50. * @brief Installation method
  51. */
  52. public function install()
  53. {
  54. Configuration::updateValue('AVALARATAX_URL', 'https://avatax.avalara.net');
  55. Configuration::updateValue('AVALARATAX_ADDRESS_VALIDATION', 1);
  56. Configuration::updateValue('AVALARATAX_TAX_CALCULATION', 1);
  57. Configuration::updateValue('AVALARATAX_TIMEOUT', 300);
  58. // Value possible : Development / Production
  59. Configuration::updateValue('AVALARATAX_MODE', 'Production');
  60. Configuration::updateValue('AVALARATAX_ADDRESS_NORMALIZATION', 1);
  61. Configuration::updateValue('AVALARATAX_COMMIT_ID', (int)Configuration::get('PS_OS_DELIVERED'));
  62. Configuration::updateValue('AVALARATAX_CANCEL_ID', (int)Configuration::get('PS_OS_CANCELED'));
  63. Configuration::updateValue('AVALARATAX_REFUND_ID', (int)Configuration::get('PS_OS_REFUND'));
  64. Configuration::updateValue('AVALARATAX_POST_ID', (int)Configuration::get('PS_OS_PAYMENT'));
  65. Configuration::updateValue('AVALARATAX_STATE', 1);
  66. Configuration::updateValue('PS_TAX_DISPLAY', 1);
  67. Configuration::updateValue('AVALARATAX_COUNTRY', 0);
  68. Configuration::updateValue('AVALARA_CACHE_MAX_LIMIT', 3600); /* The values in cache will be refreshed every 1 minute by default */
  69. // Make sure Avalara Tables don't exist before installation
  70. Db::getInstance()->Execute('DROP TABLE IF EXISTS `'._DB_PREFIX_.'avalara_product_cache`');
  71. Db::getInstance()->Execute('DROP TABLE IF EXISTS `'._DB_PREFIX_.'avalara_carrier_cache`');
  72. Db::getInstance()->Execute('DROP TABLE IF EXISTS `'._DB_PREFIX_.'avalara_address_validation_cache`');
  73. Db::getInstance()->Execute('DROP TABLE IF EXISTS `'._DB_PREFIX_.'avalara_returned_products`');
  74. Db::getInstance()->Execute('DROP TABLE IF EXISTS `'._DB_PREFIX_.'avalara_temp`');
  75. if (!Db::getInstance()->Execute('
  76. CREATE TABLE `'._DB_PREFIX_.'avalara_product_cache` (
  77. `id_cache` int(10) unsigned NOT NULL auto_increment,
  78. `id_product` int(10) unsigned NOT NULL,
  79. `tax_rate` float(8, 2) unsigned NOT NULL,
  80. `region` varchar(2) NOT NULL,
  81. `id_address` int(10) unsigned NOT NULL,
  82. `update_date` datetime,
  83. PRIMARY KEY (`id_cache`),
  84. UNIQUE (`id_product`, `region`),
  85. KEY `id_product2` (`id_product`,`region`,`id_address`))
  86. ENGINE='._MYSQL_ENGINE_.' DEFAULT CHARSET=utf8') ||
  87. !Db::getInstance()->Execute('
  88. CREATE TABLE `'._DB_PREFIX_.'avalara_carrier_cache` (
  89. `id_cache` int(10) unsigned NOT NULL auto_increment,
  90. `id_carrier` int(10) unsigned NOT NULL,
  91. `tax_rate` float(8, 2) unsigned NOT NULL,
  92. `region` varchar(2) NOT NULL,
  93. `amount` float(8, 2) unsigned NOT NULL,
  94. `update_date` datetime,
  95. `id_cart` int(10) unsigned NOT NULL,
  96. `cart_hash` varchar(32) DEFAULT NULL,
  97. PRIMARY KEY (`id_cache`),
  98. KEY `cart_hash` (`cart_hash`),
  99. KEY `cart_idx` (`id_cart`, `id_carrier`, `region`))
  100. ENGINE='._MYSQL_ENGINE_.' DEFAULT CHARSET=utf8') ||
  101. !Db::getInstance()->Execute('
  102. CREATE TABLE `'._DB_PREFIX_.'avalara_address_validation_cache` (
  103. `id_avalara_address_validation_cache` int(10) unsigned NOT NULL auto_increment,
  104. `id_address` int(10) unsigned NOT NULL,
  105. `date_add` datetime,
  106. PRIMARY KEY (`id_avalara_address_validation_cache`),
  107. UNIQUE (`id_address`))
  108. ENGINE='._MYSQL_ENGINE_.' DEFAULT CHARSET=utf8') ||
  109. !Db::getInstance()->Execute('
  110. CREATE TABLE `'._DB_PREFIX_.'avalara_returned_products` (
  111. `id_returned_product` int(10) unsigned NOT NULL auto_increment,
  112. `id_order` int(10) unsigned NOT NULL,
  113. `id_product` int(10) unsigned NOT NULL,
  114. `total` float(8, 2) unsigned NOT NULL,
  115. `quantity` int(10) unsigned NOT NULL,
  116. `name` varchar(255) NOT NULL,
  117. `description_short` varchar(255) NULL,
  118. `tax_code` varchar(255) NULL,
  119. PRIMARY KEY (`id_returned_product`))
  120. ENGINE='._MYSQL_ENGINE_.' DEFAULT CHARSET=utf8') ||
  121. !Db::getInstance()->Execute('
  122. CREATE TABLE `'._DB_PREFIX_.'avalara_temp` (
  123. `id_order` int(10) unsigned NOT NULL,
  124. `id_order_detail` int(10) unsigned NOT NULL)
  125. ENGINE='._MYSQL_ENGINE_.' DEFAULT CHARSET=utf8') ||
  126. !Db::getInstance()->Execute('
  127. CREATE TABLE IF NOT EXISTS `'._DB_PREFIX_.'avalara_taxcodes` (
  128. `id_taxcode` int(10) unsigned NOT NULL auto_increment,
  129. `id_product` int(10) unsigned NOT NULL,
  130. `tax_code` varchar(30) NOT NULL,
  131. `taxable` int(2) unsigned NOT NULL DEFAULT 1,
  132. PRIMARY KEY (`id_taxcode`),
  133. UNIQUE (`id_product`))
  134. ENGINE='._MYSQL_ENGINE_.' DEFAULT CHARSET=utf8'))
  135. return false;
  136. if (!parent::install() || !$this->registerHook('leftColumn') || !$this->registerHook('updateOrderStatus') ||
  137. !$this->registerHook('cancelProduct') || !$this->registerHook('adminOrder') || !$this->registerHook('backOfficeTop') ||
  138. !$this->registerHook('header') || !$this->overrideFiles())
  139. return false;
  140. return true;
  141. }
  142. public function uninstall()
  143. {
  144. if (!$this->removeOverrideFiles() || !parent::uninstall() ||
  145. !Configuration::deleteByName('AVALARATAX_URL') ||
  146. !Configuration::deleteByName('AVALARATAX_ADDRESS_VALIDATION') ||
  147. !Configuration::deleteByName('AVALARATAX_TAX_CALCULATION') ||
  148. !Configuration::deleteByName('AVALARATAX_TIMEOUT') ||
  149. !Configuration::deleteByName('AVALARATAX_MODE') ||
  150. !Configuration::deleteByName('AVALARATAX_ACCOUNT_NUMBER') ||
  151. !Configuration::deleteByName('AVALARATAX_COMPANY_CODE') ||
  152. !Configuration::deleteByName('AVALARATAX_LICENSE_KEY') ||
  153. !Configuration::deleteByName('AVALARATAX_ADDRESS_NORMALIZATION') ||
  154. !Configuration::deleteByName('AVALARATAX_ADDRESS_LINE1') ||
  155. !Configuration::deleteByName('AVALARATAX_ADDRESS_LINE2') ||
  156. !Configuration::deleteByName('AVALARATAX_CITY') ||
  157. !Configuration::deleteByName('AVALARATAX_STATE') ||
  158. !Configuration::deleteByName('AVALARATAX_ZIP_CODE') ||
  159. !Configuration::deleteByName('AVALARATAX_COUNTRY') ||
  160. !Configuration::deleteByName('AVALARATAX_COMMIT_ID') ||
  161. !Configuration::deleteByName('AVALARATAX_CANCEL_ID') ||
  162. !Configuration::deleteByName('AVALARATAX_REFUND_ID') ||
  163. !Configuration::deleteByName('AVALARA_CACHE_MAX_LIMIT') ||
  164. !Configuration::deleteByName('AVALARATAX_POST_ID') ||
  165. !Configuration::deleteByName('AVALARATAX_CONFIGURATION_OK') ||
  166. !Db::getInstance()->Execute('DROP TABLE `'._DB_PREFIX_.'avalara_product_cache`') ||
  167. !Db::getInstance()->Execute('DROP TABLE `'._DB_PREFIX_.'avalara_carrier_cache`') ||
  168. !Db::getInstance()->Execute('DROP TABLE `'._DB_PREFIX_.'avalara_address_validation_cache`') ||
  169. !Db::getInstance()->Execute('DROP TABLE `'._DB_PREFIX_.'avalara_returned_products`') ||
  170. !Db::getInstance()->Execute('DROP TABLE `'._DB_PREFIX_.'avalara_temp`'))
  171. return false;
  172. // Do not remove taxcode table
  173. return true;
  174. }
  175. /**
  176. * @brief Describe the override schema
  177. */
  178. protected static function getOverrideInfo()
  179. {
  180. return array(
  181. 'Tax.php' => array(
  182. 'source' => 'override/classes/tax/Tax.php',
  183. 'dest' => 'override/classes/Tax.php',
  184. 'md5' => array(
  185. '1.1' => '5d9e318d673bfa723b02f14f952e0a7a',
  186. '2.3' => '86c900cd6fff286aa6a52df2ff72228a',
  187. '3.0.2' => 'c558c0b15877980134e301af34e42c3e',
  188. )
  189. ),
  190. 'Cart.php' => array(
  191. 'source' => 'override/classes/Cart.php',
  192. 'dest' => 'override/classes/Cart.php',
  193. 'md5' => array(
  194. '3.0.2' => 'e4b05425b6dc61f75aad434265f3cac8',
  195. '3.0.3' => 'f7388cb50fbfd300c9f81cc407b7be83',
  196. )
  197. ),
  198. 'AddressController.php' => array(
  199. 'source' => 'override/controllers/front/AddressController.php',
  200. 'dest' => 'override/controllers/AddressController.php',
  201. 'md5' => array(
  202. '1.1' => 'ebc4f31298395c4b113c7e2d7cc41b4a',
  203. '3.0.2' => 'ff3d9cb2956c35f4229d5277cb2e92e6',
  204. '3.2.1' => 'bc34c1150f7170d3ec7912eb383cd04b',
  205. )
  206. ),
  207. 'AuthController.php' => array(
  208. 'source' => 'override/controllers/front/AuthController.php',
  209. 'dest' => 'override/controllers/AuthController.php',
  210. 'md5' => array(
  211. '1.1' => '7304d7af971b30f2dcd401b80bbdf805',
  212. '3.0.2' => '3eb86260a7c8d6cfa1d209fb3e8f8bd6',
  213. )
  214. ),
  215. );
  216. }
  217. protected function removeOverrideFiles()
  218. {
  219. /** In v1.5, we do not remove override files */
  220. if (version_compare(_PS_VERSION_, '1.5', '<'))
  221. foreach (self::getOverrideInfo() as $key => $params)
  222. {
  223. if (!file_exists(_PS_ROOT_DIR_.'/'.$params['dest']))
  224. continue;
  225. $md5 = md5_file(_PS_ROOT_DIR_.'/'.$params['dest']);
  226. $removed = false;
  227. foreach ($params['md5'] as $hash)
  228. if ($md5 == $hash)
  229. {
  230. if (unlink(_PS_ROOT_DIR_.'/'.$params['dest']))
  231. $removed = true;
  232. break;
  233. }
  234. if (!$removed)
  235. $this->_errors[] = $this->l('Error while removing override: ').$key;
  236. }
  237. return !isset($this->_errors) || !$this->_errors || !count($this->_errors);
  238. }
  239. protected function overrideFiles()
  240. {
  241. /** In v1.5, we do not copy the override files */
  242. if (version_compare(_PS_VERSION_, '1.5', '<') && $this->removeOverrideFiles())
  243. {
  244. /** Check if the override directories exists */
  245. if (!is_dir(_PS_ROOT_DIR_.'/override/classes/'))
  246. mkdir(_PS_ROOT_DIR_.'/override/classes/', 0777, true);
  247. if (!is_dir(_PS_ROOT_DIR_.'/override/controllers/'))
  248. mkdir(_PS_ROOT_DIR_.'/override/controllers/', 0777, true);
  249. foreach (self::getOverrideInfo() as $key => $params)
  250. if (file_exists(_PS_ROOT_DIR_.'/'.$params['dest']))
  251. $this->_errors[] = $this->l('This override file already exists, please merge it manually: ').$key;
  252. elseif (!copy(_PS_MODULE_DIR_.'avalaratax/'.$params['source'], _PS_ROOT_DIR_.'/'.$params['dest']))
  253. $this->_erroors[] = $this->l('Error while copying the override file: ').$key;
  254. }
  255. return !isset($this->_errors) || !$this->_errors || !count($this->_errors);
  256. }
  257. /******************************************************************/
  258. /** Hook Methods **************************************************/
  259. /******************************************************************/
  260. public function hookAdminOrder($params)
  261. {
  262. $this->purgeTempTable();
  263. }
  264. public function hookCancelProduct($params)
  265. {
  266. if (isset($_POST['cancelProduct']))
  267. {
  268. $order = new Order((int)$_POST['id_order']);
  269. if (!Validate::isLoadedObject($order))
  270. return false;
  271. if ($order->invoice_number)
  272. {
  273. // Get all the cancel product's IDs
  274. $cancelledIdsOrderDetail = array();
  275. foreach ($_POST['cancelQuantity'] as $idOrderDetail => $qty)
  276. if ($qty > 0)
  277. $cancelledIdsOrderDetail[] = (int)$idOrderDetail;
  278. $cancelledIdsOrderDetail = implode(', ', $cancelledIdsOrderDetail);
  279. // Fill temp table
  280. Db::getInstance()->Execute('INSERT INTO '._DB_PREFIX_.'avalara_temp (`id_order`, `id_order_detail`)
  281. VALUES ('.(int)$_POST['id_order'].', '.(int)$params['id_order_detail'].')');
  282. // Check if we are at the end of the loop
  283. $totalLoop = Db::getInstance()->ExecuteS('SELECT COUNT(`id_order`) as totalLines
  284. FROM `'._DB_PREFIX_.'avalara_temp`
  285. WHERE `id_order_detail` IN ('.pSQL($cancelledIdsOrderDetail).')');
  286. if ($totalLoop[0]['totalLines'] != count(array_filter($_POST['cancelQuantity'])))
  287. return false;
  288. // Clean the temp table because we are at the end of the loop
  289. $this->purgeTempTable();
  290. // Get details for cancelledIdsOrderDetail (Grab the info to post to Avalara in English.)
  291. $cancelledProdIdsDetails = Db::getInstance()->ExecuteS('SELECT od.`product_id` as id_product, od.`id_order_detail`, pl.`name`,
  292. pl.`description_short`, od.`product_price` as price, od.`reduction_percent`,
  293. od.`reduction_amount`, od.`product_quantity` as quantity, atc.`tax_code`
  294. FROM '._DB_PREFIX_.'order_detail od
  295. LEFT JOIN '._DB_PREFIX_.'product p ON (p.id_product = od.product_id)
  296. LEFT JOIN '._DB_PREFIX_.'product_lang pl ON (pl.id_product = p.id_product)
  297. LEFT JOIN '._DB_PREFIX_.'avalara_taxcodes atc ON (atc.id_product = p.id_product)
  298. WHERE pl.`id_lang` = '.(int)Configuration::get('PS_LANG_DEFAULT').' AND od.`id_order` = '.(int)$_POST['id_order'].'
  299. AND od.`id_order_detail` IN ('.pSQL($cancelledIdsOrderDetail).')');
  300. // Build the product list
  301. $products = array();
  302. foreach ($cancelledProdIdsDetails as $cancelProd)
  303. $products[] = array('id_product' => (int)$cancelProd['id_product'],
  304. 'quantity' => (int)$_POST['cancelQuantity'][$cancelProd['id_order_detail']],
  305. 'total' => pSQL($_POST['cancelQuantity'][$cancelProd['id_order_detail']] * ($cancelProd['price'] - ($cancelProd['price'] * ($cancelProd['reduction_percent'] / 100)) - $cancelProd['reduction_amount'])), // Including those product with discounts
  306. 'name' => pSQL(Tools::safeOutput($cancelProd['name'])),
  307. 'description_short' => pSQL(Tools::safeOutput($cancelProd['description_short']), true),
  308. 'tax_code' => pSQL(Tools::safeOutput($cancelProd['tax_code'])));
  309. // Send to Avalara
  310. $commitResult = $this->getTax($products, array('type' => 'ReturnInvoice', 'DocCode' => (int)$_POST['id_order']));
  311. if ($commitResult['ResultCode'] == 'Warning' || $commitResult['ResultCode'] == 'Error' || $commitResult['ResultCode'] == 'Exception')
  312. echo $this->_displayConfirmation($this->l('The following error was generated while cancelling the orders you selected. <br /> - '.
  313. Tools::safeOutput($commitResult['Messages']['Summary'])), 'error');
  314. else
  315. {
  316. $this->commitToAvalara(array('id_order' => (int)$_POST['id_order']));
  317. echo $this->_displayConfirmation($this->l('The products you selected were cancelled.'));
  318. }
  319. }
  320. }
  321. }
  322. protected function getDestinationAddress($id_order)
  323. {
  324. $order = new Order((int)$id_order);
  325. if (!Validate::isLoadedObject($order))
  326. return false;
  327. $address = new Address((int)$order->{Configuration::get('PS_TAX_ADDRESS_TYPE')});
  328. if (!Validate::isLoadedObject($address))
  329. return false;
  330. $state = null;
  331. if (!empty($address->id_state))
  332. {
  333. $state = new State((int)$address->id_state);
  334. if (!Validate::isLoadedObject($state))
  335. return false;
  336. }
  337. return array($address, $state, $order);
  338. }
  339. public function hookUpdateOrderStatus($params)
  340. {
  341. list($params['address'], $params['state'], $params['order']) = self::getDestinationAddress((int)$params['id_order']);
  342. if ($params['newOrderStatus']->id == (int)Configuration::get('AVALARATAX_COMMIT_ID'))
  343. return $this->commitToAvalara($params);
  344. elseif ($params['newOrderStatus']->id == (int)Configuration::get('AVALARATAX_CANCEL_ID'))
  345. {
  346. $params['CancelCode'] = 'V';
  347. $this->cancelFromAvalara($params);
  348. return $this->cancelFromAvalara($params);
  349. }
  350. elseif ($params['newOrderStatus']->id == (int)Configuration::get('AVALARATAX_POST_ID'))
  351. return $this->postToAvalara($params);
  352. elseif ($params['newOrderStatus']->id == (int)Configuration::get('AVALARATAX_REFUND_ID'))
  353. return $this->commitToAvalara($params);
  354. return false;
  355. }
  356. public function hookBackOfficeTop()
  357. {
  358. if (Tools::isSubmit('submitAddproduct') || Tools::isSubmit('submitAddproductAndStay'))
  359. Db::getInstance()->Execute('REPLACE INTO `'._DB_PREFIX_.'avalara_taxcodes` (`id_product`, `tax_code`)
  360. VALUES ('.(isset($_GET['id_product']) ? (int)$_GET['id_product'] : 0).', \''.pSQL(Tools::safeOutput($_POST['tax_code'])).'\')');
  361. if ((isset($_GET['updateproduct']) || isset($_GET['addproduct'])) && isset($_GET['id_product']) && (int)$_GET['id_product'])
  362. {
  363. $r = Db::getInstance()->getRow('
  364. SELECT `tax_code`
  365. FROM `'._DB_PREFIX_.'avalara_taxcodes` atc
  366. WHERE atc.`id_product` = '.(int)Tools::getValue('id_product'));
  367. if (version_compare(_PS_VERSION_, '1.5', '<')) /* v1.4.x an older */
  368. {
  369. return '
  370. <script type="text/javascript">
  371. $(function() {
  372. // Add the Tax Code field
  373. $(\'<tr><td class="col-left">'.$this->l('Tax Code (Avalara)').':</td><td style="padding-bottom:5px;"><input type="text" style="width: 130px; margin-right: 5px;" value="'.
  374. ($r ? Tools::safeOutput($r['tax_code']) : '').'" name="tax_code" maxlength="13" size="55"></td></tr>\').appendTo(\'#product #step1 table:eq(0) tbody\');
  375. // override original tax rules
  376. $(\'span #id_tax_rules_group\').parent().html(\'Avalara\');
  377. });
  378. </script>';
  379. }
  380. elseif (version_compare(_PS_VERSION_, '1.6', '<')) /* v1.5.x */
  381. {
  382. return '
  383. <script type="text/javascript">
  384. $(function() {
  385. var done = false;
  386. // Add the Tax Code field
  387. $(\'#link-Informations\').click(function() {
  388. if (done == false) {
  389. done = true;
  390. $(\'<tr><td class="col-left"><label for="tax_code">'.$this->l('Tax Code:').'</label></td><td style="padding-bottom:5px;"><input type="text" style="width: 130px; margin-right: 5px;" value="'.
  391. ($r ? Tools::safeOutput($r['tax_code']) : '').'" name="tax_code" maxlength="13" size="55"> <span class="small">(Avalara)</span></td></tr>\').appendTo(\'#step1 table:first tbody\');
  392. }
  393. });
  394. // override original tax rules
  395. $(\'#link-Prices\').click(function() {
  396. $(\'span #id_tax_rules_group\').parent().html(\'Avalara\');
  397. });
  398. });
  399. </script>';
  400. }
  401. else /* v1.6.x and newer */
  402. {
  403. return '
  404. <script type="text/javascript">
  405. $(function() {
  406. var done = false;
  407. // Add the Tax Code field
  408. $(\'#link-Prices\').click(function() {
  409. if (done == false) {
  410. done = true;
  411. $(\'#id_tax_rules_group\').parent().parent().parent().parent().html(\'<div class="form-group"><label class="control-label col-lg-3" for="tax_code"><span class="label-tooltip" data-toggle="tooltip" title="" data-original-title="'.$this->l('Tax rules will be handled by Avalara').'">'.$this->l('Tax Code (Avalara):').'</span></label><div class="input-group col-lg-4"><input type="text" value="'.($r ? Tools::safeOutput($r['tax_code']) : '').'" name="tax_code" maxlength="13" /><div class="alert alert-info" style="margin-top: 40px;">'.$this->l('Tax rules will be handled by Avalara').'</div></div>\');
  412. }
  413. });
  414. });
  415. </script>';
  416. }
  417. }
  418. elseif ((Tools::isSubmit('updatecarrier') || Tools::isSubmit('addcarrier')) && Tools::getValue('id_carrier'))
  419. return '<script type="text/javascript">
  420. $(function() {
  421. // override original tax rules
  422. $(\'div #id_tax_rules_group\').parent().html(\'<label class="t">Avalara</label>\');
  423. });
  424. </script>';
  425. if (Tools::getValue('tab') == 'AdminTaxes' || Tools::getValue('tab') == 'AdminTaxRulesGroup' || Tools::getValue('controller') == 'admintaxes' || Tools::getValue('controller') == 'admintaxrulesgroup')
  426. {
  427. // JS for 1.5
  428. if (version_compare(_PS_VERSION_, '1.5', '>'))
  429. return '<script type="text/javascript">
  430. $(function() {
  431. $(\'#content form\').hide();
  432. $(\'#desc-tax-new\').hide();
  433. $(\'#desc-tax-save\').hide();
  434. $(\'#content div:first\').append(\'<div class="warn">'.$this->l('Tax rules are overwritten by Avalara Tax Module.').'</div>\');
  435. });
  436. </script>';
  437. // JS for 1.4
  438. return '<script type="text/javascript">
  439. $(function() {
  440. if ($(\'#Taxes\').size() || $(\'#submitFiltertax_rules_group\').size())
  441. $(\'#content\').prepend(\'<div class="warn"><img src="../img/admin/warn2.png">'.
  442. $this->l('Tax rules are overwritten by Avalara Tax Module.').'</div>\');
  443. });
  444. </script>';
  445. }
  446. return '';
  447. }
  448. public function hookHeader()
  449. {
  450. if (!$this->context->cart || ((int)$this->context->cart->id_customer && !(int)$this->context->cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')}))
  451. $id_address = (int)(Db::getInstance()->getValue('SELECT `id_address` FROM `'._DB_PREFIX_.'address` WHERE `id_customer` = '.(int)($this->context->cart->id_customer).' AND `deleted` = 0 ORDER BY `id_address`'));
  452. else
  453. $id_address = $this->context->cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')};
  454. if (!(int)$id_address)
  455. return ;
  456. return '<script type="text/javascript">
  457. function refresh_taxes(count)
  458. {
  459. $.ajax({
  460. type : \'POST\',
  461. url : \''.$this->_path.'\' + \'ajax.php\',
  462. data : {
  463. \'id_cart\': "'.(int)$this->context->cart->id.'",
  464. \'id_lang\': "'.(int)$this->context->cookie->id_lang.'",
  465. \'id_address\': "'.(int)$id_address.'",
  466. \'ajax\': "getProductTaxRate",
  467. \'token\': "'.md5(_COOKIE_KEY_.Configuration::get('PS_SHOP_NAME')).'",
  468. },
  469. dataType: \'json\',
  470. success : function(d) {
  471. if (d.hasError == false && d.cached_tax == true)
  472. $(\'#total_tax\').html($(\'body\').data(\'total_tax\'));
  473. else if (d.hasError == false && d.cached_tax == false)
  474. $(\'#total_tax\').html(d.total_tax);
  475. else
  476. $(\'#total_tax\').html(\''.$this->l('Error while calculating taxes').'\');
  477. }
  478. });
  479. }
  480. $(function() {
  481. /* ajax call to cache taxes product taxes that exist in the current cart */
  482. $(\'body\').data(\'total_tax\', $(\'#total_tax\').text());
  483. $(\'#total_tax\').html(\'<img src="img/loader.gif" alt="" />\');
  484. refresh_taxes(1);
  485. });
  486. </script>';
  487. }
  488. /******************************************************************/
  489. /** Main Form Methods *********************************************/
  490. /******************************************************************/
  491. public function getContent()
  492. {
  493. $buffer = '';
  494. if (version_compare(_PS_VERSION_,'1.5','>'))
  495. $this->context->controller->addJQueryPlugin('fancybox');
  496. else
  497. $buffer .= '<script type="text/javascript" src="'.__PS_BASE_URI__.'js/jquery/jquery.fancybox-1.3.4.js"></script>
  498. <link type="text/css" rel="stylesheet" href="'.__PS_BASE_URI__.'css/jquery.fancybox-1.3.4.css" />';
  499. if (Tools::isSubmit('SubmitAvalaraTaxSettings'))
  500. {
  501. Configuration::updateValue('AVALARATAX_ACCOUNT_NUMBER', Tools::getValue('avalaratax_account_number'));
  502. Configuration::updateValue('AVALARATAX_LICENSE_KEY', Tools::getValue('avalaratax_license_key'));
  503. Configuration::updateValue('AVALARATAX_URL', Tools::getValue('avalaratax_url'));
  504. Configuration::updateValue('AVALARATAX_COMPANY_CODE', Tools::getValue('avalaratax_company_code'));
  505. $connectionTestResult = $this->_testConnection();
  506. if (strpos($connectionTestResult[0], 'Error') === false)
  507. {
  508. Configuration::updateValue('AVALARATAX_CONFIGURATION_OK', true);
  509. $buffer .= $this->_displayConfirmation();
  510. }
  511. }
  512. elseif (Tools::isSubmit('SubmitAvalaraTaxOptions'))
  513. {
  514. Configuration::updateValue('AVALARATAX_ADDRESS_VALIDATION', Tools::getValue('avalaratax_address_validation'));
  515. Configuration::updateValue('AVALARATAX_TAX_CALCULATION', Tools::getValue('avalaratax_tax_calculation'));
  516. Configuration::updateValue('AVALARATAX_TIMEOUT', (int)Tools::getValue('avalaratax_timeout'));
  517. Configuration::updateValue('AVALARATAX_ADDRESS_NORMALIZATION', Tools::getValue('avalaratax_address_normalization'));
  518. Configuration::updateValue('AVALARATAX_TAX_OUTSIDE', Tools::getValue('avalaratax_tax_outside'));
  519. Configuration::updateValue('AVALARA_CACHE_MAX_LIMIT', (int)Tools::getValue('avalara_cache_max_limit'));
  520. $buffer .= $this->_displayConfirmation();
  521. }
  522. elseif (Tools::isSubmit('SubmitAvalaraTestConnection'))
  523. $connectionTestResult = $this->_testConnection();
  524. elseif (Tools::isSubmit('SubmitAvalaraAddressOptions'))
  525. {
  526. /* Validate address*/
  527. $address = new Address();
  528. $address->address1 = Tools::getValue('avalaratax_address_line1');
  529. $address->address2 = Tools::getValue('avalaratax_address_line2');
  530. $address->city = Tools::getValue('avalaratax_city');
  531. $address->id_state = State::getIdByIso(Tools::getValue('avalaratax_state'));
  532. $address->id_country = Tools::getValue('avalaratax_country');
  533. $address->postcode = Tools::getValue('avalaratax_zip_code');
  534. $normalizedAddress = $this->validateAddress($address);
  535. if (isset($normalizedAddress['ResultCode']) && $normalizedAddress['ResultCode'] == 'Success')
  536. {
  537. $buffer .= $this->_displayConfirmation($this->l('The address you submitted has been validated.'));
  538. Configuration::updateValue('AVALARATAX_ADDRESS_LINE1', $normalizedAddress['Normalized']['Line1']);
  539. Configuration::updateValue('AVALARATAX_ADDRESS_LINE2', $normalizedAddress['Normalized']['Line2']);
  540. Configuration::updateValue('AVALARATAX_CITY', $normalizedAddress['Normalized']['City']);
  541. Configuration::updateValue('AVALARATAX_STATE', $normalizedAddress['Normalized']['Region']);
  542. Configuration::updateValue('AVALARATAX_COUNTRY', $normalizedAddress['Normalized']['Country']);
  543. Configuration::updateValue('AVALARATAX_ZIP_CODE', $normalizedAddress['Normalized']['PostalCode']);
  544. }
  545. else
  546. {
  547. $message = $this->l('The following error was generated while validating your address:');
  548. if (isset($normalizedAddress['Exception']['FaultString']))
  549. $message .= '<br /> - '.Tools::safeOutput($normalizedAddress['Exception']['FaultString']);
  550. if (isset($normalizedAddress['Messages']['Summary']))
  551. foreach ($normalizedAddress['Messages']['Summary'] as $summary)
  552. $message .= '<br /> - '.Tools::safeOutput($summary);
  553. $buffer .= $this->_displayConfirmation($message, 'error');
  554. Configuration::updateValue('AVALARATAX_ADDRESS_LINE1', Tools::getValue('avalaratax_address_line1'));
  555. Configuration::updateValue('AVALARATAX_ADDRESS_LINE2', Tools::getValue('avalaratax_address_line2'));
  556. Configuration::updateValue('AVALARATAX_CITY', Tools::getValue('avalaratax_city'));
  557. Configuration::updateValue('AVALARATAX_STATE', Tools::getValue('avalaratax_state'));
  558. Configuration::updateValue('AVALARATAX_ZIP_CODE', Tools::getValue('avalaratax_zip_code'));
  559. }
  560. }
  561. elseif (Tools::isSubmit('SubmitAvalaraTaxClearCache'))
  562. {
  563. Db::getInstance()->Execute('TRUNCATE TABLE `'._DB_PREFIX_.'avalara_product_cache`');
  564. Db::getInstance()->Execute('TRUNCATE TABLE `'._DB_PREFIX_.'avalara_carrier_cache`');
  565. $buffer .= $this->_displayConfirmation('Cache cleared!');
  566. }
  567. $confValues = Configuration::getMultiple(array(
  568. // Configuration
  569. 'AVALARATAX_ACCOUNT_NUMBER', 'AVALARATAX_LICENSE_KEY', 'AVALARATAX_URL', 'AVALARATAX_COMPANY_CODE',
  570. // Options
  571. 'AVALARATAX_ADDRESS_VALIDATION', 'AVALARATAX_TAX_CALCULATION', 'AVALARATAX_TIMEOUT',
  572. 'AVALARATAX_ADDRESS_NORMALIZATION', 'AVALARATAX_TAX_OUTSIDE', 'AVALARATAX_COMMIT_ID', 'AVALARATAX_CANCEL_ID',
  573. 'AVALARATAX_REFUND_ID', 'AVALARATAX_POST_ID', 'AVALARA_CACHE_MAX_LIMIT',
  574. // Default Address
  575. 'AVALARATAX_ADDRESS_LINE1', 'AVALARATAX_ADDRESS_LINE2', 'AVALARATAX_CITY', 'AVALARATAX_STATE',
  576. 'AVALARATAX_ZIP_CODE', 'AVALARATAX_COUNTRY'));
  577. $stateList = array();
  578. $stateList[] = array('id' => '0', 'name' => $this->l('Choose your state (if applicable)'), 'iso_code' => '--');
  579. foreach (State::getStates((int)$this->context->cookie->id_lang) as $state)
  580. $stateList[] = array('id' => $state['id_state'], 'name' => $state['name'], 'iso_code' => $state['iso_code']);
  581. $countryList = array();
  582. $countryList[] = array('id' => '0', 'name' => $this->l('Choose your country'), 'iso_code' => '--');
  583. foreach (Country::getCountries((int)$this->context->cookie->id_lang, false, null, false) as $country)
  584. $countryList[] = array('id' => $country['id_country'], 'name' => $country['name'], 'iso_code' => $country['iso_code']);
  585. $buffer .= '<link href="'.$this->_path.'css/avalara.css" rel="stylesheet" type="text/css">
  586. <script type="text/javascript">
  587. /* Fancybox */
  588. $(\'a.avalara-video-btn\').live(\'click\', function(){
  589. $.fancybox({
  590. \'type\' : \'iframe\',
  591. \'href\' : this.href.replace(new RegExp("watch\\?v=", "i"), \'embed\') + \'?rel=0&autoplay=1\',
  592. \'swf\': {\'allowfullscreen\':\'true\', \'wmode\':\'transparent\'},
  593. \'overlayShow\' : true,
  594. \'centerOnScroll\' : true,
  595. \'speedIn\' : 100,
  596. \'speedOut\' : 50,
  597. \'width\' : 853,
  598. \'height\' : 480
  599. });
  600. return false;
  601. });
  602. </script>
  603. <script type="text/javascript">
  604. $(document).ready(function(){
  605. var height1 = 0;
  606. var height = 0;
  607. $(\'.field-height1\').each(function(){
  608. if (height1 < $(this).height())
  609. height1 = $(this).height();
  610. });
  611. $(\'.field-height\').each(function(){
  612. if (height < $(this).height())
  613. height = $(this).height();
  614. });
  615. $(\'.field-height1\').css({\'height\' : $(\'.field-height1\').css(\'height\', height1+\'px\')});
  616. $(\'.field-height\').css({\'height\' : $(\'.field-height\').css(\'height\', height+\'px\')});
  617. updateAvalaraTaxState($(\'#avalaratax_country\').val());
  618. $(\'#avalaratax_country\').change(function(){
  619. updateAvalaraTaxState($(this).val());
  620. });
  621. });
  622. function updateAvalaraTaxState(iso_code)
  623. {
  624. var default_state = "'.$confValues['AVALARATAX_STATE'].'";
  625. $(\'#avalaratax_state\').html(\'\');
  626. $.ajax({
  627. type : \'GET\',
  628. url : \'../modules/avalaratax/states.php?country_iso_code=\'+iso_code,
  629. dataType: \'JSON\',
  630. success: function(data)
  631. {
  632. if (data != 0)
  633. {
  634. $.each(data[iso_code], function(i, item){
  635. if (default_state == item.state_iso_code)
  636. $(\'#avalaratax_state\').append(\'<option selected="selected" value="\'+item.state_iso_code+\'">\'+item.name+\'</option>\');
  637. else
  638. $(\'#avalaratax_state\').append(\'<option value="\'+item.state_iso_code+\'">\'+item.name+\'</option>\');
  639. $(\'#avalaratax_state\').show();
  640. $(\'#avalaratax_label_state\').show();
  641. });
  642. }
  643. else
  644. {
  645. $(\'#avalaratax_state\').hide();
  646. $(\'#avalaratax_label_state\').hide();
  647. }
  648. }
  649. });
  650. }
  651. </script>
  652. <div class="avalara-wrap">
  653. <p class="avalara-intro"><a href="http://www.info.avalara.com/prestashop" class="avalara-logo" target="_blank"><img src="'.$this->_path.'img/avalara_logo.png" alt="Avalara" border="0" /></a><a href="http://www.info.avalara.com/prestashop" class="avalara-link" target="_blank">'.$this->l('Create an account').'</a>'.$this->l('Avalara and PrestaShop have partnered to provide the easiest way for you to accurately calculate and file sales tax.').'</p>
  654. <div class="clear"></div>
  655. <div class="avalara-content">
  656. <div class="avalara-video">
  657. <a href="http://www.youtube.com/embed/tm1tENVdcQ8" class="avalara-video-btn"><img src="'.$this->_path.'img/avalara-video-screen.jpg" alt="Avalara Video" /><img src="'.$this->_path.'img/btn-video.png" alt="" class="video-icon" /></a>
  658. </div>
  659. <h3>'.$this->l('Doing sales tax right is simple with Avalara.').'</h3>
  660. <p>'.$this->l('We do all of the research and automate the process for you, ensuring that the system is up-to-date with the most recent sales tax and VAT rates and rules in every state and country, so you don’t have to. As a cloud-based service, AvaTax eliminates ongoing maintenance and support. It provides you with a complete solution to manage your sales tax needs.').'</p>
  661. <img src="'.$this->_path.'img/avatax_badge.png" alt="AvaTax Certified" class="avatax-badge" />
  662. <ul>
  663. <li>'.$this->l('Address Validation included').'</li>
  664. <li>'.$this->l('Rooftop Accurate Calculations').'</li>
  665. <li>'.$this->l('Product and Service Taxability Rules').'</li>
  666. <li>'.$this->l('Exemption Certificate Management').'</li>
  667. <li>'.$this->l('Out-of-the-Box Sales Tax Reporting').'</li>
  668. </ul>
  669. <a href="http://www.info.avalara.com/prestashop" class="avalara-link" target="_blank">'.$this->l('Create an account').'</a>
  670. <p class="contact-avalara"><a href="http://www.info.avalara.com/prestashop" target="_blank">'.$this->l('Contact Avalara Today to Start Your Service').'</a></p>
  671. </div>
  672. <fieldset class="field-height1 right-fieldset">
  673. <legend><img src="'.$this->_path.'img/icon-console.gif" alt="" />'.$this->l('AvaTax Admin Console').'</legend>
  674. <p><a href="https://admin-avatax.avalara.net/" target="_blank">'.$this->l('Log-in to AvaTax Admin Console').'</a></p>
  675. <a href="https://admin-avatax.avalara.net/" target="_blank"><img src="'.$this->_path.'img/avatax-logo.png" alt="AvaTax" class="avatax-logo" /></a>
  676. </fieldset>
  677. <form action="'.Tools::safeOutput($_SERVER['REQUEST_URI']).'" method="post" class="left-form">
  678. <fieldset class="field-height1">
  679. <legend><img src="'.$this->_path.'img/icon-config.gif" alt="" />'.$this->l('Configuration').'</legend>
  680. <h4>'.$this->l('AvaTax Credentials').'</h4>';
  681. if (isset($connectionTestResult))
  682. $buffer .= '<div id="test_connection" style="background: '.Tools::safeOutput($connectionTestResult[1]).';">'.$connectionTestResult[0].'</div>';
  683. $buffer .= '<label>'.$this->l('Account Number').'</label>
  684. <div class="margin-form">
  685. <input type="text" name="avalaratax_account_number" value="'.(isset($confValues['AVALARATAX_ACCOUNT_NUMBER']) ? Tools::safeOutput($confValues['AVALARATAX_ACCOUNT_NUMBER']) : '').'" />
  686. </div>
  687. <label>'.$this->l('License Key').'</label>
  688. <div class="margin-form">
  689. <input type="text" name="avalaratax_license_key" value="'.(isset($confValues['AVALARATAX_LICENSE_KEY']) ? Tools::safeOutput($confValues['AVALARATAX_LICENSE_KEY']) : '').'" />
  690. </div>
  691. <label>'.$this->l('URL').'</label>
  692. <div class="margin-form">
  693. <input type="text" name="avalaratax_url" value="'.(isset($confValues['AVALARATAX_URL']) ? Tools::safeOutput($confValues['AVALARATAX_URL']) : '').'" />
  694. </div>
  695. <label>'.$this->l('Company Code').'</label>
  696. <div class="margin-form">
  697. <input type="text" name="avalaratax_company_code" value="'.(isset($confValues['AVALARATAX_COMPANY_CODE']) ? Tools::safeOutput($confValues['AVALARATAX_COMPANY_CODE']) : '').'" /> '.$this->l('Located in the top-right corner of your AvaTax Admin Console').'
  698. </div>
  699. <div class="margin-form">
  700. <input type="submit" class="button" name="SubmitAvalaraTaxSettings" value="'.$this->l('Save Settings').'" /><img src="'.$this->_path.'img/icon-connection.gif" alt="" class="icon-connection" /><input type="submit" id="avalaratax_test_connection" class="button" name="SubmitAvalaraTestConnection" value="'.$this->l('Click here to Test Connection').'" />
  701. </div>
  702. </fieldset>
  703. </form>
  704. <form action="'.Tools::safeOutput($_SERVER['REQUEST_URI']).'" method="post" class="form-half reset-label">
  705. <fieldset class="field-height MR7">
  706. <legend><img src="'.$this->_path.'img/icon-options.gif" alt="" />'.$this->l('Options').'</legend>
  707. <label>'.$this->l('Enable address validation').'</label>
  708. <div class="margin-form">
  709. <input type="checkbox" name="avalaratax_address_validation" value="1"'.(isset($confValues['AVALARATAX_ADDRESS_VALIDATION']) && $confValues['AVALARATAX_ADDRESS_VALIDATION'] ? ' checked="checked"' : '').' />
  710. ('.$this->l('Not compatible with One Page Checkout').')
  711. </div>
  712. <label>'.$this->l('Enable tax calculation').'</label>
  713. <div class="margin-form">
  714. <input type="checkbox" name="avalaratax_tax_calculation" value="1" '.(isset($confValues['AVALARATAX_TAX_CALCULATION']) && $confValues['AVALARATAX_TAX_CALCULATION'] ? ' checked="checked"' : '').' />
  715. </div>
  716. <label>'.$this->l('Enable address normalization in uppercase').'</label>
  717. <div class="margin-form">
  718. <input type="checkbox" name="avalaratax_address_normalization" value="1" '.(isset($confValues['AVALARATAX_ADDRESS_NORMALIZATION']) && $confValues['AVALARATAX_ADDRESS_NORMALIZATION'] ? ' checked="checked"' : '').' />
  719. </div>
  720. <label>'.$this->l('Enable tax calculation outside of your state').'</label>
  721. <div class="margin-form">
  722. <input type="checkbox" name="avalaratax_tax_outside" value="1" '.(isset($confValues['AVALARATAX_TAX_OUTSIDE']) && $confValues['AVALARATAX_TAX_OUTSIDE'] ? ' checked="checked"' : '').' />
  723. </div>
  724. <label>'.$this->l('Request timeout').'</label>
  725. <div class="margin-form">
  726. <input type="text" name="avalaratax_timeout" value="'.(isset($confValues['AVALARATAX_TIMEOUT']) ? Tools::safeOutput($confValues['AVALARATAX_TIMEOUT']) : '').'" style="width: 40px;" /> '.$this->l('seconds').'
  727. </div>
  728. <label>'.$this->l('Refresh tax rate cache every x seconds (default 3600):').'</label>
  729. <div class="margin-form">
  730. <input type="text" name="avalara_cache_max_limit" value="'.(isset($confValues['AVALARA_CACHE_MAX_LIMIT']) ? (int)Tools::safeOutput($confValues['AVALARA_CACHE_MAX_LIMIT']) : '').'" style="width: 40px;" /> '.$this->l('seconds').'
  731. </div>
  732. <div class="margin-form">
  733. <input type="submit" class="button avalaratax_button" name="SubmitAvalaraTaxOptions" value="'.$this->l('Save Settings').'" />
  734. <input type="submit" class="button avalaratax_button" name="SubmitAvalaraTaxClearCache" value="'.$this->l('Clear Cache').'" style="display: none;"/>
  735. </div>
  736. <div class="sep"></div>
  737. <h4>'.$this->l('Default Post/Commit/Cancel/Refund Options').'</h4>
  738. <span class="avalara-info">'.$this->l('When an order\'s status is updated, the following options will be used to update Avalara\'s records.').'</span>';
  739. // Check if the order status exist
  740. $orderStatusList = array();
  741. foreach (Db::getInstance()->ExecuteS('SELECT `id_order_state`, `name` FROM `'._DB_PREFIX_.'order_state_lang` WHERE `id_lang` = '.(int)$this->context->cookie->id_lang) as $v)
  742. $orderStatusList[$v['id_order_state']] = Tools::safeOutput($v['name']);
  743. $buffer .= '<table class="avalara-table" cellspacing="0" cellpadding="0" width="100%">
  744. <th>'.$this->l('Action').'</th>
  745. <th>'.$this->l('Order status in your store').'</th>
  746. <tr>
  747. <td class="avalaratax_column">'.$this->l('Post order to Avalara').':</td>
  748. <td>'.(isset($orderStatusList[Configuration::get('AVALARATAX_POST_ID')]) ? html_entity_decode(Tools::safeOutput($orderStatusList[Configuration::get('AVALARATAX_POST_ID')])) :
  749. '<div style="color: red">'.$this->l('[ERROR] A default value was not found. Please, restore PrestaShop\'s default statuses.').'</div>').'
  750. </td>
  751. </tr>
  752. <tr>
  753. <td class="avalaratax_column">'.$this->l('Commit order to Avalara').':</td>
  754. <td>'.(isset($orderStatusList[Configuration::get('AVALARATAX_COMMIT_ID')]) ? html_entity_decode(Tools::safeOutput($orderStatusList[Configuration::get('AVALARATAX_COMMIT_ID')])) :
  755. '<div style="color: red">'.$this->l('[ERROR] A default value was not found. Please, restore PrestaShop\'s default statuses.').'</div>').'
  756. </td>
  757. </tr>
  758. <tr>
  759. <td class="avalaratax_column">'.$this->l('Delete order from Avalara').':</td>
  760. <td>'.(isset($orderStatusList[Configuration::get('AVALARATAX_CANCEL_ID')]) ? html_entity_decode(Tools::safeOutput($orderStatusList[Configuration::get('AVALARATAX_CANCEL_ID')])) :
  761. '<div style="color: red">'.$this->l('[ERROR] A default value was not found. Please, restore PrestaShop\'s default statuses.').'</div>').'
  762. </td>
  763. </tr>
  764. <tr>
  765. <td class="avalaratax_column last">'.$this->l('Void order in Avalara').':</td>
  766. <td class="last">'.(isset($orderStatusList[Configuration::get('AVALARATAX_REFUND_ID')]) ? html_entity_decode(Tools::safeOutput($orderStatusList[Configuration::get('AVALARATAX_REFUND_ID')])) :
  767. '<div style="color: red">'.$this->l('[ERROR] A default value was not found. Please, restore PrestaShop\'s default statuses.').'</div>').'
  768. </td>
  769. </tr>
  770. </table>
  771. </fieldset>
  772. </form>
  773. <form action="'.Tools::safeOutput($_SERVER['REQUEST_URI']).'" method="post" class="form-half">
  774. <fieldset class="field-height ML7">
  775. <legend><img src="'.$this->_path.'img/icon-address.gif" alt="" />'.$this->l('Default Origin Address and Tax Information').'</legend>
  776. <label>'.$this->l('Address Line 1').'</label>
  777. <div class="margin-form">
  778. <input type="text" name="avalaratax_address_line1" value="'.(isset($confValues['AVALARATAX_ADDRESS_LINE1']) ? Tools::safeOutput($confValues['AVALARATAX_ADDRESS_LINE1']) : '').'" />
  779. </div>
  780. <label>'.$this->l('Address Line 2').'</label>
  781. <div class="margin-form">
  782. <input type="text" name="avalaratax_address_line2" value="'.(isset($confValues['AVALARATAX_ADDRESS_LINE2']) ? Tools::safeOutput($confValues['AVALARATAX_ADDRESS_LINE2']) : '').'" />
  783. </div>
  784. <label>'.$this->l('City').'</label>
  785. <div class="margin-form">
  786. <input type="text" name="avalaratax_city" value="'.(isset($confValues['AVALARATAX_CITY']) ? Tools::safeOutput($confValues['AVALARATAX_CITY']) : '').'" />
  787. </div>
  788. <label>'.$this->l('Zip Code').'</label>
  789. <div class="margin-form">
  790. <input type="text" name="avalaratax_zip_code" value="'.(isset($confValues['AVALARATAX_ZIP_CODE']) ? Tools::safeOutput($confValues['AVALARATAX_ZIP_CODE']) : '').'" />
  791. </div>
  792. <label>'.$this->l('Country').'</label>
  793. <div class="margin-form">
  794. <select name="avalaratax_country" id="avalaratax_country">';
  795. foreach ($countryList as $country)
  796. $buffer .= '<option value="'.substr(strtoupper($country['iso_code']), 0, 2).'" '.($country['iso_code'] == $confValues['AVALARATAX_COUNTRY'] ? ' selected="selected"' : '').'>'.Tools::safeOutput($country['name']).'</option>';
  797. $buffer .= '</select>
  798. </div>
  799. <label id="avalaratax_label_state" >'.$this->l('State').'</label>
  800. <div class="margin-form">
  801. <select name="avalaratax_state" id="avalaratax_state">';
  802. foreach ($stateList as $state)
  803. $buffer .= '<option value="'.substr(strtoupper($state['iso_code']), 0, 2).'" '.($state['iso_code'] == $confValues['AVALARATAX_STATE'] ? ' selected="selected"' : '').'>'.Tools::safeOutput($state['name']).'</option>';
  804. $buffer .= '</select>
  805. </div>
  806. <div class="margin-form">
  807. <input type="submit" class="button" name="SubmitAvalaraAddressOptions" value="'.$this->l('Save Settings').'" />
  808. </div>
  809. </fieldset>
  810. </form>
  811. <div class="clear"></div>
  812. </div>';
  813. return $buffer;
  814. }
  815. /*
  816. ** Display a custom message for settings update
  817. ** $text string Text to be displayed in the message
  818. ** $type string (confirm|warn|error) Decides what color will the message have (green|yellow)
  819. */
  820. private function _displayConfirmation($text = '', $type = 'confirm')
  821. {
  822. if ($type == 'confirm')
  823. $img = 'ok.gif';
  824. elseif ($type == 'warn')
  825. $img = 'warn2.png';
  826. elseif ($type == 'error')
  827. $img = 'disabled.gif';
  828. else
  829. die('Invalid type.');
  830. return '<div class="conf '.Tools::safeOutput($type).'">
  831. <img src="../img/admin/'.$img.'" alt="" title="" />
  832. '.(empty($text) ? $this->l('Settings updated') : $text).
  833. '<img src="http://www.prestashop.com/modules/avalaratax.png?sid='.urlencode(Configuration::get('AVALARATAX_ACCOUNT_NUMBER')).'" style="float: right;" />
  834. </div>';
  835. }
  836. /**
  837. * @brief init the Avatax SDK
  838. */
  839. private function _connectToAvalara()
  840. {
  841. $timeout = Configuration::get('AVALARATAX_TIMEOUT');
  842. if ((int)$timeout > 0)
  843. ini_set('max_execution_time', (int)$timeout);
  844. include_once(dirname(__FILE__).'/sdk/AvaTax.php');
  845. /* Just instantiate the ATConfig class to init the settings (mandatory...) */
  846. new ATConfig(Configuration::get('AVALARATAX_MODE'), array('url' => Configuration::get('AVALARATAX_URL'), 'account' => Configuration::get('AVALARATAX_ACCOUNT_NUMBER'),
  847. 'license' => Configuration::get('AVALARATAX_LICENSE_KEY'), 'trace' => false));
  848. }
  849. /**
  850. * @brief Connect to Avalara to make sure everything is OK
  851. */
  852. private function _testConnection()
  853. {
  854. $this->_connectToAvalara();
  855. try
  856. {
  857. $client = new TaxServiceSoap(Configuration::get('AVALARATAX_MODE'));
  858. $connectionTest = $client->ping();
  859. if ($connectionTest->getResultCode() == SeverityLevel::$Success)
  860. {
  861. try
  862. {
  863. $authorizedTest = $client->isAuthorized('GetTax');
  864. if ($authorizedTest->getResultCode() == SeverityLevel::$Success)
  865. $expirationDate = $authorizedTest->getexpires();
  866. }
  867. catch (SoapFault $exception)
  868. {
  869. }
  870. return array('<img src="../img/admin/ok.gif" alt="" /><strong style="color: green;">'.$this->l('Connection Test performed successfully.').'</strong><br /><br />'.$this->l('Ping version is:').' '.Tools::safeOutput($connectionTest->getVersion()).(isset($expirationDate) ? '<br /><br />'.$this->l('License Expiration Date:').' '.Tools::safeOutput($expirationDate) : ''), '#D6F5D6');
  871. }
  872. }
  873. catch (SoapFault $exception)
  874. {
  875. return array('<img src="../img/admin/forbbiden.gif" alt="" /><b style="color: #CC0000;">'.$this->l('Connection Test Failed.').'</b><br /><br />'.$this->l('Either the Account or License Key is incorrect. Please confirm the Account and License Key before testing the connection again.').'<br /><br /><strong style="color: #CC0000;">'.$this->l('Error(s):').' '.Tools::safeOutput($exception->faultstring).'</strong>', '#FFD8D8');
  876. }
  877. }
  878. /**
  879. * @brief Validates a given address
  880. */
  881. public function validateAddress(Address $address)
  882. {
  883. $this->_connectToAvalara();
  884. $client = new AddressServiceSoap(Configuration::get('AVALARATAX_MODE'));
  885. if (!empty($address->id_state))
  886. $state = new State((int)$address->id_state);
  887. if (!empty($address->id_country))
  888. $country = new Country((int)$address->id_country);
  889. $avalaraAddress = new AvalaraAddress($address->address1, $address->address2, null, $address->city,
  890. (isset($state) ? $state->iso_code : null), $address->postcode, (isset($country) ? $country->iso_code : null), 0);
  891. $buffer = array();
  892. try
  893. {
  894. $request = new ValidateRequest($avalaraAddress, TextCase::$Upper, false);
  895. $result = $client->Validate($request);
  896. $addresses = $result->ValidAddresses;
  897. $buffer['ResultCode'] = Tools::safeOutput($result->getResultCode());
  898. if ($result->getResultCode() != SeverityLevel::$Success)
  899. foreach ($result->getMessages() as $msg)
  900. {
  901. $buffer['Messages']['Name'][] = Tools::safeOutput($msg->getName());
  902. $buffer['Messages']['Summary'][] = Tools::safeOutput($msg->getSummary());
  903. }
  904. else
  905. foreach ($result->getvalidAddresses() as $valid)
  906. {
  907. $buffer['Normalized']['Line1'] = Tools::safeOutput($valid->getline1());
  908. $buffer['Normalized']['Line2'] = Tools::safeOutput($valid->getline2());
  909. $buffer['Normalized']['City']= Tools::safeOutput($valid->getcity());
  910. $buffer['Normalized']['Region'] = Tools::safeOutput($valid->getregion());
  911. $buffer['Normalized']['PostalCode'] = Tools::safeOutput($valid->getpostalCode());
  912. $buffer['Normalized']['Country'] = Tools::safeOutput($valid->getcountry());
  913. $buffer['Normalized']['County'] = Tools::safeOutput($valid->getcounty());
  914. $buffer['Normalized']['FIPS'] = Tools::safeOutput($valid->getfipsCode());
  915. $buffer['Normalized']['PostNet'] = Tools::safeOutput($valid->getpostNet());
  916. $buffer['Normalized']['CarrierRoute'] = Tools::safeOutput($valid->getcarrierRoute());
  917. $buffer['Normalized']['AddressType'] = Tools::safeOutput($valid->getaddressType());
  918. }
  919. }
  920. catch (SoapFault $exception)
  921. {
  922. $buffer['Exception']['FaultString'] = Tools::safeOutput($exception->faultstring);
  923. $buffer['Exception']['LastRequest'] = Tools::safeOutput($client->__getLastRequest());
  924. $buffer['Exception']['LastResponse'] = Tools::safeOutput($client->__getLastResponse());
  925. }
  926. return $buffer;
  927. }
  928. /**
  929. * @brief Executes tax actions on documents
  930. *
  931. * @param Array $products Array of Product for which taxes need to be calculated
  932. * @param Array $params
  933. * type : (default SalesOrder) SalesOrder|SalesInvoice|ReturnInvoice
  934. * cart : (required for SalesOrder and SalesInvoice) Cart object
  935. * DocCode : (required in ReturnInvoice, and when 'cart' is not set) Specify the Document Code
  936. *
  937. */
  938. public function getTax($products = array(), $params = array())
  939. {
  940. $confValues = Configuration::getMultiple(array('AVALARATAX_COMPANY_CODE', 'AVALARATAX_ADDRESS_LINE1',
  941. 'AVALARATAX_ADDRESS_LINE2', 'AVALARATAX_CITY', 'AVALARATAX_STATE', 'AVALARATAX_ZIP_CODE'));
  942. if (!isset($params['type']))
  943. $params['type'] = 'SalesOrder';
  944. $this->_connectToAvalara();
  945. $client = new TaxServiceSoap(Configuration::get('AVALARATAX_MODE'));
  946. $request = new GetTaxRequest();
  947. if (isset($this->context->cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')}))
  948. $address = new Address((int)$this->context->cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')});
  949. elseif (isset($this->context->cookie) && isset($this->context->cookie->id_customer) && $this->context->cookie->id_customer)
  950. $address = new Address((int)Db::getInstance()->getValue('SELECT `id_address` FROM `'._DB_PREFIX_.'address` WHERE `id_customer` = '.(int)$this->context->cookie->id_customer.' AND active = 1 AND deleted = 0'));
  951. if (isset($address))
  952. {
  953. if (!empty($address->id_state))
  954. $state = new State((int)$address->id_state);
  955. $addressDest = array();
  956. $addressDest['Line1'] = $address->address1;
  957. $addressDest['Line2'] = $address->address2;
  958. $addressDest['City'] = $address->city;
  959. $addressDest['Region'] = isset($state) ? $state->iso_code : '';
  960. $addressDest['PostalCode'] = $address->postcode;
  961. $addressDest['Country'] = Country::getIsoById($address->id_country);
  962. // Try to normalize the address depending on option in the BO
  963. if (Configuration::get('AVALARATAX_ADDRESS_NORMALIZATION'))
  964. {
  965. $last_update = Db::getInstance()->getValue('SELECT date_add FROM '._DB_PREFIX_.'avalara_address_validation_cache WHERE id_address = '.(int)$address->id);
  966. if (empty($last_update) || (strtotime($address->date_upd) > strtotime($last_update)))
  967. {
  968. $normalizedAddress = $this->validateAddress($address);
  969. Db::getInstance()->Execute('INSERT INTO '._DB_PREFIX_.'avalara_address_validation_cache (id_address, date_add) VALUES ('.(int)$address->id.', \''.pSQL(date('Y-m-d H:i:s')).'\') ON DUPLICATE KEY UPDATE date_add = \''.pSQL(date('Y-m-d H:i:s')).'\'');
  970. }
  971. }
  972. if (isset($normalizedAddress['Normalized']))
  973. $addressDest = $normalizedAddress['Normalized'];
  974. // Add Destination address (Customer address)
  975. $destination = new AvalaraAddress();
  976. $destination->setLine1($addressDest['Line1']);
  977. $destination->setLine2($addressDest['Line2']);
  978. $destination->setCity($addressDest['City']);
  979. $destination->setRegion($addressDest['Region']);
  980. $destination->setPostalCode($addressDest['PostalCode']);
  981. $destination->setCountry($addressDest['Country']);
  982. $request->setDestinationAddress($destination);
  983. }
  984. // Origin Address (Store Address or address setup in BO)
  985. $origin = new AvalaraAddress();
  986. $origin->setLine1(isset($confValues['AVALARATAX_ADDRESS_LINE1']) ? $confValues['AVALARATAX_ADDRESS_LINE1'] : '');
  987. $origin->setLine2(isset($confValues['AVALARATAX_ADDRESS_LINE2']) ? $confValues['AVALARATAX_ADDRESS_LINE2'] : '');
  988. $origin->setCity(isset($confValues['AVALARATAX_CITY']) ? $confValues['AVALARATAX_CITY'] : '');
  989. $origin->setRegion(isset($confValues['AVALARATAX_STATE']) ? $confValues['AVALARATAX_STATE'] : '');
  990. $origin->setPostalCode(isset($confValues['AVALARATAX_ZIP_CODE']) ? $confValues['AVALARATAX_ZIP_CODE'] : '');
  991. $request->setOriginAddress($origin);
  992. $request->setCompanyCode(isset($confValues['AVALARATAX_COMPANY_CODE']) ? $confValues['AVALARATAX_COMPANY_CODE'] : '');
  993. if (isset($address->vat_number) && !empty($address->vat_number) && $address->vat_number != 'undefined')
  994. $request->setBusinessIdentificationNo($address->vat_number);
  995. $orderId = isset($params['cart']) ? (int)$params['cart']->id : (int)$params['DocCode'];
  996. $nowTime = date('mdHis');
  997. // Type: Only supported types are SalesInvoice or SalesOrder
  998. if ($params['type'] == 'SalesOrder') // SalesOrder: Occurs when customer adds product to the cart (generally to check how much the tax will be)
  999. $request->setDocType(DocumentType::$SalesOrder);
  1000. elseif ($params['type'] == 'SalesInvoice') // SalesInvoice: Occurs when customer places an order (It works like commitToAvalara()).
  1001. {
  1002. $request->setDocType(DocumentType::$SalesInvoice);
  1003. $orderId = Db::getInstance()->getValue('SELECT `id_order` FROM '._DB_PREFIX_.'orders WHERE `id_cart` = '.(int)$params['cart']->id); // Make sure we got the orderId, even if it was/wasn't passed in $params['DocCode']
  1004. }
  1005. elseif ($params['type'] == 'ReturnInvoice')
  1006. {
  1007. $orderId = isset($params['type']) && $params['type'] == 'ReturnInvoice' ? $orderId.'.'.$nowTime : $orderId;
  1008. $orderDate = Db::getInstance()->ExecuteS('
  1009. SELECT `id_order`, `date_add`
  1010. FROM `'._DB_PREFIX_.'orders`
  1011. WHERE '.(isset($params['cart']) ? '`id_cart` = '.(int)$params['cart']->id : '`id_order` = '.(int)$params['DocCode']));
  1012. $request->setDocType(DocumentType::$ReturnInvoice);
  1013. $request->setCommit(true);
  1014. $taxOverride = new TaxOverride();
  1015. $taxOverride->setTaxOverrideType(TaxOverrideType::$TaxDate);
  1016. $taxOverride->setTaxDate(date('Y-m-d', strtotime($orderDate[0]['date_add'])));
  1017. $taxOverride->setReason('Refund');
  1018. $request->setTaxOverride($taxOverride);
  1019. }
  1020. if (isset($this->context->cookie->id_customer))
  1021. $customerCode = $this->context->cookie->id_customer;
  1022. else
  1023. {
  1024. if (isset($params['DocCode']))
  1025. $id_order = (int)$params['DocCode'];
  1026. elseif (isset($_POST['id_order']))
  1027. $id_order = (int)$_POST['id_order'];
  1028. elseif (isset($params['id_order']))
  1029. $id_order = (int)$params['id_order'];
  1030. else
  1031. $id_order = 0;
  1032. $customerCode = (int)Db::getInstance()->getValue('SELECT `id_customer` FROM `'._DB_PREFIX_.'orders` WHERE `id_order` = '.(int)$id_order);
  1033. }
  1034. $request->setDocCode('Order '.Tools::safeOutput($orderId)); // Order Id - has to be float due to the . and more numbers for returns
  1035. $request->setDocDate(date('Y-m-d')); // date
  1036. $request->setCustomerCode('CustomerID: '.(int)$customerCode); // string Required
  1037. $request->setDiscount($params['cart']->getOrderTotal(true, Cart::ONLY_DISCOUNTS)); // decimal
  1038. $request->setDetailLevel(DetailLevel::$Tax); // Summary or Document or Line or Tax or Diagnostic
  1039. // Add line
  1040. $lines = array();
  1041. $i = 0;
  1042. foreach ($products as $product)
  1043. {
  1044. // Retrieve the tax_code for the current product if not defined
  1045. if (isset($params['taxable']) && !$params['taxable'])
  1046. $taxCode = 'NT';
  1047. else
  1048. $taxCode = !isset($product['tax_code']) ? $this->getProductTaxCode((int)$product['id_product']) : $product['tax_code'];
  1049. if (isset($product['id_product']))
  1050. {
  1051. $line = new Line();
  1052. $line->setNo($i++); // string line Number of invoice ($i)
  1053. $line->setItemCode((int)$product['id_product'].' - '.substr($product['name'], 0, 20));
  1054. $line->setDescription(substr(Tools::safeOutput($product['name'].' - '.$product['description_short']), 0, 250));
  1055. $line->setTaxCode($taxCode);
  1056. $line->setQty(isset($product['quantity']) ? (float)$product['quantity'] : 1);
  1057. $line->setAmount($params['type'] == 'ReturnInvoice' && (float)$product['total'] > 0 ? (float)$product['total'] * -1 : (float)$product['total']);
  1058. $line->setDiscounted(false);
  1059. $lines[] = $line;
  1060. }
  1061. }
  1062. // Send shipping as new line
  1063. if (isset($params['cart']))
  1064. {
  1065. $line = new Line();
  1066. $line->setNo('Shipping'); // string line Number of invoice ($i)
  1067. $line->setItemCode('Shipping');
  1068. $line->setDescription('Shipping costs');
  1069. if (isset($params['taxable']) && !$params['taxable'])
  1070. $line->setTaxCode('NT');
  1071. else
  1072. $line->setTaxCode('FR020100'); // Default TaxCode for Shipping. Avalara will decide depending on the State if taxes should be charged or not
  1073. $line->setQty(1);
  1074. $line->setAmount((float)$params['cart']->getOrderTotal(false, Cart::ONLY_SHIPPING));
  1075. $line->setDiscounted(false);
  1076. $lines[] = $line;
  1077. }
  1078. $request->setLines($lines);
  1079. $buffer = array();
  1080. try
  1081. {
  1082. $result = $client->getTax($request);
  1083. $buffer['ResultCode'] = Tools::safeOutput($result->getResultCode());
  1084. if ($result->getResultCode() == SeverityLevel::$Success)
  1085. {
  1086. $buffer['DocCode'] = Tools::safeOutput($request->getDocCode());
  1087. $buffer['TotalAmount'] = Tools::safeOutput($result->getTotalAmount());
  1088. $buffer['TotalTax'] = Tools::safeOutput($result->getTotalTax());
  1089. $buffer['NowTime'] = $nowTime;
  1090. foreach ($result->getTaxLines() as $ctl)
  1091. {
  1092. $buffer['TaxLines'][$ctl->getNo()]['GetTax'] = Tools::safeOutput($ctl->getTax());
  1093. $buffer['TaxLines'][$ctl->getNo()]['TaxCode'] = Tools::safeOutput($ctl->getTaxCode());
  1094. foreach ($ctl->getTaxDetails() as $ctd)
  1095. {
  1096. $buffer['TaxLines'][$ctl->getNo()]['TaxDetails']['JurisType'] = Tools::safeOutput($ctd->getJurisType());
  1097. $buffer['TaxLines'][$ctl->getNo()]['TaxDetails']['JurisName'] = Tools::safeOutput($ctd->getJurisName());
  1098. $buffer['TaxLines'][$ctl->getNo()]['TaxDetails']['Region'] = Tools::safeOutput($ctd->getRegion());
  1099. $buffer['TaxLines'][$ctl->getNo()]['TaxDetails']['Rate'] = Tools::safeOutput($ctd->getRate());
  1100. $buffer['TaxLines'][$ctl->getNo()]['TaxDetails']['Tax'] = Tools::safeOutput($ctd->getTax());
  1101. }
  1102. }
  1103. }
  1104. else
  1105. foreach ($result->getMessages() as $msg)
  1106. {
  1107. $buffer['Messages']['Name'] = Tools::safeOutput($msg->getName());
  1108. $buffer['Messages']['Summary'] = Tools::safeOutput($msg->getSummary());
  1109. }
  1110. }
  1111. catch (SoapFault $exception)
  1112. {
  1113. $buffer['Exception']['FaultString'] = Tools::safeOutput($exception->faultstring);
  1114. $buffer['Exception']['LastRequest'] = Tools::safeOutput($client->__getLastRequest());
  1115. $buffer['Exception']['LastResponse'] = Tools::safeOutput($client->__getLastResponse());
  1116. }
  1117. return $buffer;
  1118. }
  1119. /*
  1120. ** Make changes to an order, get order history or checks if the module is authorized
  1121. **
  1122. ** $type string commit|post|cancel|history Transaction type
  1123. ** $params array Key=>Values depending on the transaction type
  1124. ** DocCode: (required for ALL except for isAuthorized) Document unique identifier
  1125. ** DocDate: (required for post) Date in which the transaction was made (today's date if post)
  1126. ** IdCustomer: (required for post) Customer ID
  1127. ** TotalAmount: (required for post) Order total amount in case of Post type
  1128. ** TotalTax: (required for post) Total tax amount for current order
  1129. ** CancelCode: (required for cancel only) D|P Sets the cancel code (D: Document Deleted | P: Post Failed)
  1130. */
  1131. public function tax($type, $params = array())
  1132. {
  1133. $this->_connectToAvalara();
  1134. $client = new TaxServiceSoap(Configuration::get('AVALARATAX_MODE'));
  1135. if ($type == 'commit')
  1136. $request= new CommitTaxRequest();
  1137. elseif ($type == 'post')
  1138. {
  1139. $request= new PostTaxRequest();
  1140. $request->setDocDate($params['DocDate']);
  1141. $request->setTotalAmount($params['TotalAmount']);
  1142. $request->setTotalTax($params['TotalTax']);
  1143. }
  1144. elseif ($type == 'cancel')
  1145. {
  1146. $request= new CancelTaxRequest();
  1147. if ($params['CancelCode'] == 'D')
  1148. $code = CancelCode::$DocDeleted;
  1149. elseif ($params['CancelCode'] == 'P')
  1150. $code = CancelCode::$PostFailed;
  1151. elseif ($params['CancelCode'] == 'V')
  1152. $code = CancelCode::$DocVoided;
  1153. else
  1154. die('Invalid cancel code.');
  1155. $request->setCancelCode($code);
  1156. }
  1157. elseif ($type == 'history')
  1158. {
  1159. $request= new GetTaxHistoryRequest();
  1160. $request->setDetailLevel(DetailLevel::$Document);
  1161. }
  1162. if ($type != 'isAuthorized')
  1163. {
  1164. $request->setDocCode('Order '.(int)$params['DocCode']);
  1165. $request->setDocType(DocumentType::$SalesInvoice);
  1166. $request->setCompanyCode(Configuration::get('AVALARATAX_COMPANY_CODE'));
  1167. }
  1168. $buffer = array();
  1169. try
  1170. {
  1171. if ($type == 'commit')
  1172. $result = $client->commitTax($request);
  1173. elseif ($type == 'post')
  1174. $result = $client->postTax($request);
  1175. elseif ($type == 'cancel')
  1176. $result = $client->cancelTax($request);
  1177. elseif ($type == 'isAuthorized')
  1178. $result = $client->isAuthorized('GetTax');
  1179. elseif ($type == 'history')
  1180. {
  1181. $result = $client->getTaxHistory($request);
  1182. $buffer['Invoice'] = $result->getGetTaxRequest()->getDocCode();
  1183. $buffer['Status'] = $result->getGetTaxResult()->getDocStatus();
  1184. }
  1185. $buffer['ResultCode'] = $result->getResultCode();
  1186. if ($result->getResultCode() != SeverityLevel::$Success)
  1187. foreach ($result->getMessages() as $msg)
  1188. {
  1189. $buffer['Messages']['Name'] = Tools::safeOutput($msg->getName());
  1190. $buffer['Messages']['Summary'] = Tools::safeOutput($msg->getSummary());
  1191. }
  1192. }
  1193. catch (SoapFault $exception)
  1194. {
  1195. $buffer['Exception']['FaultString'] = Tools::safeOutput($exception->faultstring);
  1196. $buffer['Exception']['LastRequest'] = Tools::safeOutput($client->__getLastRequest());
  1197. $buffer['Exception']['LastResponse'] = Tools::safeOutput($client->__getLastResponse());
  1198. }
  1199. return $buffer;
  1200. }
  1201. public function postToAvalara($params)
  1202. {
  1203. if (!isset($params['address']))
  1204. list($params['address'], $params['state'], $params['order']) = self::getDestinationAddress((int)$params['id_order']);
  1205. $destination = new AvalaraAddress();
  1206. $destination->setLine1($params['address']->address1);
  1207. $destination->setLine2($params['address']->address2);
  1208. $destination->setCity($params['address']->city);
  1209. $destination->setRegion(isset($params['state']) ? $params['state']->iso_code : '');
  1210. $destination->setPostalCode($params['address']->postcode);
  1211. $commitResult = $this->tax('history', array('DocCode' => (int)$params['id_order'], 'Destination' => $destination));
  1212. if (isset($commitResult['ResultCode']) && $commitResult['ResultCode'] == 'Success')
  1213. {
  1214. $params['CancelCode'] = 'D';
  1215. $this->cancelFromAvalara($params);
  1216. $this->cancelFromAvalara($params); // Twice because first call only voids the order, and 2nd call deletes it
  1217. }
  1218. // Grab the info to post to Avalara in English.
  1219. $order = new Order((isset($_POST['id_order']) ? (int)$_POST['id_order'] : (int)$params['id_order']));
  1220. $allProducts = Db::getInstance()->ExecuteS('SELECT p.`id_product`, pl.`name`, pl.`description_short`,
  1221. od.`product_price` as price, od.`reduction_percent`,
  1222. od.`reduction_amount`, od.`product_quantity` as quantity, atc.`tax_code`
  1223. FROM `'._DB_PREFIX_.'order_detail` od
  1224. LEFT JOIN `'._DB_PREFIX_.'product` p ON (p.id_product = od.product_id)
  1225. LEFT JOIN `'._DB_PREFIX_.'product_lang` pl ON (pl.id_product = p.id_product)
  1226. LEFT JOIN `'._DB_PREFIX_.'avalara_taxcodes` atc ON (atc.id_product = p.id_product)
  1227. WHERE pl.`id_lang` = '.(int)Configuration::get('PS_LANG_DEFAULT').' AND od.`id_order` = '.(isset($_POST['id_order']) ? (int)$_POST['id_order'] : (int)$params['id_order']));
  1228. $products = array();
  1229. foreach ($allProducts as $v)
  1230. $products[] = array('id_product' => $v['id_product'],
  1231. 'name' => $v['name'],
  1232. 'description_short' => $v['description_short'],
  1233. 'quantity' => $v['quantity'],
  1234. 'total' => $v['quantity'] * ($v['price'] - ($v['price'] * ($v['reduction_percent'] / 100)) - ($v['reduction_amount'])), // Including those products with discounts
  1235. 'tax_code' => $v['tax_code'],
  1236. 'taxable' => (bool)$this->getProductTaxable((int)$v['id_product']));
  1237. $taxable = true;
  1238. //check if it is outside the state and if we are in united state and if conf AVALARATAX_TAX_OUTSIDE IS ENABLE
  1239. if (isset($params['state']) && !Configuration::get('AVALARATAX_TAX_OUTSIDE') && $params['state']->iso_code != Configuration::get('AVALARATAX_STATE'))
  1240. $taxable = false;
  1241. $cart = new Cart((int)$order->id_cart);
  1242. $getTaxResult = $this->getTax($products, array('type' => 'SalesInvoice', 'cart' => $cart, 'id_order' => isset($_POST['id_order']) ? (int)$_POST['id_order'] : (int)$params['id_order'], 'taxable' => $taxable), $params['address']->id);
  1243. $commitResult = $this->tax('post', array('DocCode' => (isset($_POST['id_order']) ? (int)$_POST['id_order'] : (int)$params['id_order']),
  1244. 'DocDate' => date('Y-m-d'), 'IdCustomer' => (int)$cart->id_customer, 'TotalAmount' => (float)$getTaxResult['TotalAmount'],
  1245. 'TotalTax' => (float)$getTaxResult['TotalTax']));
  1246. if (isset($commitResult['ResultCode']) && ($commitResult['ResultCode'] == 'Warning' || $commitResult['ResultCode'] == 'Error' || $commitResult['ResultCode'] == 'Exception'))
  1247. return $this->_displayConfirmation($this->l('The following error was generated while cancelling the orders you selected.'.
  1248. '<br /> - '.Tools::safeOutput($commitResult['Messages']['Summary'])), 'error');
  1249. return $this->_displayConfirmation($this->l('The orders you selected were posted.'));
  1250. }
  1251. public function commitToAvalara($params)
  1252. {
  1253. // Create the order before commiting to Avalara
  1254. $this->postToAvalara($params);
  1255. $commitResult = $this->tax('history', array('DocCode' => $params['id_order']));
  1256. if (isset($commitResult['ResultCode']) && $commitResult['ResultCode'] == 'Success')
  1257. {
  1258. $commitResult = $this->tax('commit', array('DocCode' => (int)$params['id_order']));
  1259. if (isset($commitResult['Exception']) || isset($commitResult['ResultCode']) && ($commitResult['ResultCode'] == 'Warning' || $commitResult['ResultCode'] == 'Error' || $commitResult['ResultCode'] == 'Exception'))
  1260. return ($this->_displayConfirmation($this->l('The following error was generated while committing the orders you selected to Avalara.').
  1261. (isset($commitResult['Messages']) ? '<br /> - '.Tools::safeOutput($commitResult['Messages']['Summary']) : '').
  1262. (isset($commitResult['Exception']) ? '<br /> - '.Tools::safeOutput($commitResult['Exception']['FaultString']) : ''), 'error'));
  1263. else
  1264. return $this->_displayConfirmation($this->l('The orders you selected were committed.'));
  1265. }
  1266. // Orders prior Avalara module installation will trigger an "Invalid Status" error. For this reason, the user won't be alerted here.
  1267. }
  1268. public function cancelFromAvalara($params)
  1269. {
  1270. $commitResult = $this->tax('history', array('DocCode' => $params['id_order']));
  1271. $hasRefund = Db::getInstance()->ExecuteS('SELECT COUNT(`id_order`) as qtyProductRefunded
  1272. FROM `ps_order_detail`
  1273. WHERE `id_order` = '.(int)$params['id_order'].'
  1274. AND (`product_quantity_refunded` IS NOT NULL AND `product_quantity_refunded` > 0)');
  1275. if (!($commitResult['Status'] == 'Committed' && (int)$hasRefund[0]['qtyProductRefunded'] > 0))
  1276. {
  1277. if (isset($commitResult['Status']) && $commitResult['Status'] == 'Temporary')
  1278. $this->postToAvalara($params);
  1279. $commitResult = $this->tax('cancel', array('DocCode' => (int)$params['id_order'],
  1280. 'CancelCode' => isset($params['CancelCode']) ? $params['CancelCode'] : 'V' ));
  1281. if (isset($commitResult['ResultCode'])
  1282. && ( $commitResult['ResultCode'] == 'Warning'
  1283. || $commitResult['ResultCode'] == 'Error'
  1284. || $commitResult['ResultCode'] == 'Exception'))
  1285. return $this->_displayConfirmation($this->l('The following error was generated while cancelling the orders you selected.').
  1286. ' <br /> - '.Tools::safeOutput($commitResult['Messages']['Summary']), 'error');
  1287. else
  1288. return $this->_displayConfirmation($this->l('The orders you selected were cancelled.'));
  1289. }
  1290. }
  1291. /*
  1292. ** Fix $_POST to validate/normalize the address on address creation/update
  1293. */
  1294. public function fixPOST()
  1295. {
  1296. /* Validate address only in the U.S. and Canada - if the Address Validation feature has been turned on in the module's configuration */
  1297. if (($address->id_country == Country::getByIso('US') || $address->id_country == Country::getByIso('CA')) && $this->tax('isAuthorized') && Configuration::get('AVALARATAX_ADDRESS_VALIDATION'))
  1298. {
  1299. $address = new Address(isset($_POST['id_address']) ? (int)$_POST['id_address'] : null);
  1300. $address->address1 = isset($_POST['address1']) ? $_POST['address1'] : null;
  1301. $address->address2 = isset($_POST['address2']) ? $_POST['address2'] : null;
  1302. $address->city = isset($_POST['city']) ? $_POST['city'] : null;
  1303. $address->region = isset($_POST['region']) ? $_POST['region'] : null;
  1304. $address->postcode = isset($_POST['postcode']) ? $_POST['postcode'] : null;
  1305. $address->id_country = isset($_POST['id_country']) ? $_POST['id_country'] : null;
  1306. $address->id_state = isset($_POST['id_state']) ? (int)$_POST['id_state'] : null;
  1307. $normalizedAddress = $this->validateAddress($address);
  1308. if (isset($normalizedAddress['ResultCode']) && $normalizedAddress['ResultCode'] == 'Success')
  1309. {
  1310. Db::getInstance()->Execute('INSERT INTO '._DB_PREFIX_.'avalara_address_validation_cache (id_address, date_add) VALUES ('.(int)$address->id.', \''.pSQL(date('Y-m-d H:i:s')).'\') ON DUPLICATE KEY UPDATE date_add = \''.pSQL(date('Y-m-d H:i:s')).'\'');
  1311. $_POST['address1'] = Tools::safeOutput($normalizedAddress['Normalized']['Line1']);
  1312. $_POST['address2'] = Tools::safeOutput($normalizedAddress['Normalized']['Line2']);
  1313. $_POST['city'] = Tools::safeOutput($normalizedAddress['Normalized']['City']);
  1314. $_POST['postcode'] = Tools::safeOutput(substr($normalizedAddress['Normalized']['PostalCode'], 0, strpos($normalizedAddress['Normalized']['PostalCode'], '-')));
  1315. }
  1316. return $normalizedAddress;
  1317. }
  1318. }
  1319. public function getProductTaxCode($id_product)
  1320. {
  1321. $result = Db::getInstance()->getValue('
  1322. SELECT `tax_code`
  1323. FROM `'._DB_PREFIX_.'avalara_taxcodes` atc
  1324. WHERE atc.`id_product` = '.(int)$id_product);
  1325. return $result ? Tools::safeOutput($result) : '0';
  1326. }
  1327. public function getProductTaxable($idProduct)
  1328. {
  1329. // !== and not != because it can fail if getProductTaxCode return an int.
  1330. return $this->getProductTaxCode($idProduct) !== 'NT';
  1331. }
  1332. private function purgeTempTable()
  1333. {
  1334. return Db::getInstance()->Execute('TRUNCATE TABLE `'._DB_PREFIX_.'avalara_temp`');
  1335. }
  1336. private function getCurrentURL($htmlEntities = false)
  1337. {
  1338. $url = Tools::safeOutput($_SERVER['SERVER_NAME'].$_SERVER['REQUEST_URI'], true);
  1339. return (!empty($_SERVER['HTTPS']) ? 'https' : 'http').'://'.($htmlEntities ? preg_replace('/&/', '&amp;', $url): $url);
  1340. }
  1341. }
  1342. function avalaraAutoload($className)
  1343. {
  1344. $className = str_replace(chr(0), '', $className);
  1345. if (!preg_match('/^\w+$/', $className))
  1346. die('Invalid classname.');
  1347. $moduleDir = dirname(__FILE__).'/';
  1348. if (file_exists($moduleDir.$className.'.php'))
  1349. require_once($moduleDir.$className.'.php');
  1350. elseif (file_exists($moduleDir.'sdk/classes/'.$className.'.class.php'))
  1351. require_once($moduleDir.'sdk/classes/'.$className.'.class.php');
  1352. elseif (file_exists($moduleDir.'sdk/classes/BatchSvc/'.$className.'.class.php'))
  1353. require_once($moduleDir.'sdk/classes/BatchSvc/'.$className.'.class.php');
  1354. elseif (function_exists('__autoload'))
  1355. __autoload($className);
  1356. }