* @copyright 2007-2011 PrestaShop SA
* @version Release: $Revision: 1.4 $
* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
* International Registered Trademark & Property of PrestaShop SA
*/
// Security
if (!defined('_PS_VERSION_'))
exit;
spl_autoload_register('avalaraAutoload');
class AvalaraTax extends Module
{
private $_overrideFilesInModule = array(
'Tax.php' => 'override/classes/Tax.php',
'AddressController.php' => 'override/controllers/AddressController.php',
'AuthController.php' => 'override/controllers/AuthController.php',
);
/******************************************************************/
/** Construct Method **********************************************/
/******************************************************************/
public function __construct()
{
global $cookie;
$this->name = 'avalaratax';
$this->tab = 'billing_invoicing';
$this->version = '1.1';
$this->author = 'PrestaShop';
$this->limited_countries = array('us', 'ca');
parent::__construct();
$this->displayName = $this->l('Avalara - AvaTax');
$this->description = $this->l('Sales Tax is complicated. AvaTax makes it easy.');
$timeout = Configuration::get('AVALARATAX_TIMEOUT');
if ((int)$timeout > 0)
ini_set('max_execution_time', (int)$timeout);
}
/******************************************************************/
/** Install / Uninstall Methods ***********************************/
/******************************************************************/
public function install()
{
Configuration::updateValue('AVALARATAX_URL', 'https://development.avalara.net');
Configuration::updateValue('AVALARATAX_ADDRESS_VALIDATION', 1);
Configuration::updateValue('AVALARATAX_TAX_CALCULATION', 1);
Configuration::updateValue('AVALARATAX_TIMEOUT', 300);
// Value possible : Development / Production
Configuration::updateValue('AVALARATAX_MODE', 'Development'); /* @todo : Before module release, change to: Production */
Configuration::updateValue('AVALARATAX_ADDRESS_NORMALIZATION', 1);
Configuration::updateValue('AVALARATAX_COMMIT_ID', 5);
Configuration::updateValue('AVALARATAX_CANCEL_ID', 6);
Configuration::updateValue('AVALARATAX_REFUND_ID', 7);
Configuration::updateValue('AVALARATAX_POST_ID', 2);
Configuration::updateValue('AVALARATAX_STATE', 0);
Configuration::updateValue('AVALARATAX_COUNTRY', 0);
Configuration::updateValue('AVALARA_CACHE_MAX_LIMIT', 1); /* The values in cache will be refreshed every 1 minute by default */
// Make sure Avalara Tables don't exist before installation
Db::getInstance()->Execute('DROP TABLE IF EXISTS `'._DB_PREFIX_.'avalara_product_cache`;');
Db::getInstance()->Execute('DROP TABLE IF EXISTS `'._DB_PREFIX_.'avalara_carrier_cache`;');
Db::getInstance()->Execute('DROP TABLE IF EXISTS `'._DB_PREFIX_.'avalara_returned_products`;');
Db::getInstance()->Execute('DROP TABLE IF EXISTS `'._DB_PREFIX_.'avalara_temp`;');
Db::getInstance()->Execute('DROP TABLE IF EXISTS `'._DB_PREFIX_.'avalara_taxcodes`;');
if (!Db::getInstance()->Execute('
CREATE TABLE `'._DB_PREFIX_.'avalara_product_cache` (
`id_cache` int(10) unsigned NOT NULL auto_increment,
`id_product` int(10) unsigned NOT NULL,
`tax_rate` float(8, 2) unsigned NOT NULL,
`region` varchar(2) NOT NULL,
`update_date` datetime,
PRIMARY KEY (`id_cache`),
UNIQUE (`id_product`, `region`))
ENGINE='._MYSQL_ENGINE_.' DEFAULT CHARSET=utf8') ||
!Db::getInstance()->Execute('
CREATE TABLE `'._DB_PREFIX_.'avalara_carrier_cache` (
`id_cache` int(10) unsigned NOT NULL auto_increment,
`id_carrier` int(10) unsigned NOT NULL,
`tax_rate` float(8, 2) unsigned NOT NULL,
`region` varchar(2) NOT NULL,
`update_date` datetime,
PRIMARY KEY (`id_cache`),
UNIQUE (`id_carrier`, `region`))
ENGINE='._MYSQL_ENGINE_.' DEFAULT CHARSET=utf8')||
!Db::getInstance()->Execute('
CREATE TABLE `'._DB_PREFIX_.'avalara_returned_products` (
`id_returned_product` int(10) unsigned NOT NULL auto_increment,
`id_order` int(10) unsigned NOT NULL,
`id_product` int(10) unsigned NOT NULL,
`total` float(8, 2) unsigned NOT NULL,
`quantity` int(10) unsigned NOT NULL,
`name` varchar(255) NOT NULL,
`description_short` varchar(255) NULL,
`tax_code` varchar(255) NULL,
PRIMARY KEY (`id_returned_product`))
ENGINE='._MYSQL_ENGINE_.' DEFAULT CHARSET=utf8')||
!Db::getInstance()->Execute('
CREATE TABLE `'._DB_PREFIX_.'avalara_temp` (
`id_order` int(10) unsigned NOT NULL,
`id_order_detail` int(10) unsigned NOT NULL)
ENGINE='._MYSQL_ENGINE_.' DEFAULT CHARSET=utf8')||
!Db::getInstance()->Execute('
CREATE TABLE `'._DB_PREFIX_.'avalara_taxcodes` (
`id_taxcode` int(10) unsigned NOT NULL auto_increment,
`id_product` int(10) unsigned NOT NULL,
`tax_code` varchar(30) NOT NULL,
PRIMARY KEY (`id_taxcode`),
UNIQUE (`id_product`))
ENGINE='._MYSQL_ENGINE_.' DEFAULT CHARSET=utf8'))
return false;
if (!parent::install() || !$this->registerHook('leftColumn')
|| !$this->registerHook('updateOrderStatus')
|| !$this->registerHook('cancelProduct')
|| !$this->registerHook('adminOrder')
|| !$this->registerHook('backOfficeTop')
|| !$this->registerHook('header')
)
return false;
/* Check the files to override */
$filesToOverride = array();
if (file_exists(dirname(__FILE__).'/../../override/classes/Tax.php'))
$filesToOverride[] = '/override/classes/Tax.php';
if (file_exists(dirname(__FILE__).'/../../override/controllers/AddressController.php'))
$filesToOverride[] = '/override/controllers/AddressController.php';
if (file_exists(dirname(__FILE__).'/../../override/controllers/AuthController.php'))
$filesToOverride[] = '/override/controllers/AuthController.php';
if (count($filesToOverride))
die($this->_displayConfirmation($this->l('The module was successfully installed but the following file(s) already exist. Please, merge file(s) manually.').
' '.implode(' ', $filesToOverride), 'warn'));
else
{
if (!is_dir(dirname(__FILE__).'/../../override/classes/'))
mkdir(dirname(__FILE__).'/../../override/classes/', 0777, true);
if (!is_dir(dirname(__FILE__).'/../../override/controllers/'))
mkdir(dirname(__FILE__).'/../../override/controllers/', 0777, true);
foreach ($this->_overrideFilesInModule as $path)
copy(dirname(__FILE__).'/'.$path, dirname(__FILE__).'/../../'.$path);
}
return true;
}
public function uninstall()
{
// Before deleting the files, make sure the md5_file matches
// We don't want to delete a file that might have other custom modifications that the user
// might have done.
foreach ($this->_overrideFilesInModule as $path)
if (md5_file(dirname(__FILE__).'/'.$path) == md5_file(dirname(__FILE__).'/../../'.$path))
@unlink(dirname(__FILE__).'/../../'.$path);
if (!parent::uninstall() OR
!Configuration::deleteByName('AVALARATAX_URL') OR
!Configuration::deleteByName('AVALARATAX_ADDRESS_VALIDATION') OR
!Configuration::deleteByName('AVALARATAX_TAX_CALCULATION') OR
!Configuration::deleteByName('AVALARATAX_TIMEOUT') OR
!Configuration::deleteByName('AVALARATAX_MODE') OR
!Configuration::deleteByName('AVALARATAX_ACCOUNT_NUMBER') OR
!Configuration::deleteByName('AVALARATAX_COMPANY_CODE') OR
!Configuration::deleteByName('AVALARATAX_LICENSE_KEY') OR
!Configuration::deleteByName('AVALARATAX_ADDRESS_NORMALIZATION') OR
!Configuration::deleteByName('AVALARATAX_ADDRESS_LINE1') OR
!Configuration::deleteByName('AVALARATAX_ADDRESS_LINE2') OR
!Configuration::deleteByName('AVALARATAX_CITY') OR
!Configuration::deleteByName('AVALARATAX_STATE') OR
!Configuration::deleteByName('AVALARATAX_ZIP_CODE') OR
!Configuration::deleteByName('AVALARATAX_COUNTRY') OR
!Configuration::deleteByName('AVALARATAX_COMMIT_ID') OR
!Configuration::deleteByName('AVALARATAX_CANCEL_ID') OR
!Configuration::deleteByName('AVALARATAX_REFUND_ID') OR
!Configuration::deleteByName('AVALARA_CACHE_MAX_LIMIT') OR
!Configuration::deleteByName('AVALARATAX_POST_ID') OR
!Db::getInstance()->Execute('DROP TABLE `'._DB_PREFIX_.'avalara_product_cache`') OR
!Db::getInstance()->Execute('DROP TABLE `'._DB_PREFIX_.'avalara_carrier_cache`') OR
!Db::getInstance()->Execute('DROP TABLE `'._DB_PREFIX_.'avalara_returned_products`') OR
!Db::getInstance()->Execute('DROP TABLE `'._DB_PREFIX_.'avalara_temp`') OR
!Db::getInstance()->Execute('DROP TABLE `'._DB_PREFIX_.'avalara_taxcodes`'))
return false;
return true;
}
/******************************************************************/
/** Hook Methods **************************************************/
/******************************************************************/
public function hookAdminOrder($params)
{
$this->purgeTempTable();
}
public function hookCancelProduct($params)
{
if(isset($_POST['cancelProduct']))
{
$order = new Order((int)$_POST['id_order']);
if ($order->invoice_number)
{
// Get all the cancel product's IDs
$cancelledIdsOrderDetail = array();
foreach ($_POST['cancelQuantity'] as $idOrderDetail => $qty)
if ($qty > 0)
$cancelledIdsOrderDetail[] = (int)$idOrderDetail;
$cancelledIdsOrderDetail = implode(', ', $cancelledIdsOrderDetail);
// Fill temp table
Db::getInstance()->Execute('INSERT INTO '._DB_PREFIX_.'avalara_temp (`id_order`, `id_order_detail`)
VALUES ('.(int)$_POST['id_order'].', '.(int)$params['id_order_detail'].')');
// Check if we are at the end of the loop
$totalLoop = Db::getInstance()->ExecuteS('SELECT COUNT(`id_order`) as totalLines
FROM '._DB_PREFIX_.'avalara_temp
WHERE `id_order_detail` IN ('.pSQL($cancelledIdsOrderDetail).')');
if ($totalLoop[0]['totalLines'] != count(array_filter($_POST['cancelQuantity'])))
return false;
// Clean the temp table because we are at the end of the loop
$this->purgeTempTable();
// Get details for cancelledIdsOrderDetail (Grab the info to post to Avalara in English.)
$cancelledProdIdsDetails = Db::getInstance()->ExecuteS('SELECT od.`product_id` as id_product, od.`id_order_detail`, pl.`name`,
pl.`description_short`, od.`product_price` as price, od.`reduction_percent`,
od.`reduction_amount`, od.`product_quantity` as quantity, atc.`tax_code`
FROM '._DB_PREFIX_.'order_detail od
LEFT JOIN '._DB_PREFIX_.'product p ON (p.id_product = od.product_id)
LEFT JOIN '._DB_PREFIX_.'product_lang pl ON (pl.id_product = p.id_product)
LEFT JOIN '._DB_PREFIX_.'avalara_taxcodes atc ON (atc.id_product = p.id_product)
WHERE pl.`id_lang` = 1 AND od.`id_order` = '.(int)$_POST['id_order'].'
AND od.`id_order_detail` IN ('.pSQL($cancelledIdsOrderDetail).')');
// Build the product list
$products = array();
foreach ($cancelledProdIdsDetails as $cancelProd)
$products[] = array('id_product' => (int)$cancelProd['id_product'],
'quantity' => (int)$_POST['cancelQuantity'][$cancelProd['id_order_detail']],
'total' => pSQL($_POST['cancelQuantity'][$cancelProd['id_order_detail']] * ($cancelProd['price'] - ($cancelProd['price'] * ($cancelProd['reduction_percent'] / 100)) - $cancelProd['reduction_amount'])), // Including those product with discounts
'name' => pSQL(Tools::safeOutput($cancelProd['name'])),
'description_short' => pSQL(Tools::safeOutput($cancelProd['description_short']), true),
'tax_code' => pSQL(Tools::safeOutput($cancelProd['tax_code'])));
// Send to Avalara
$commitResult = $this->getTax($products, array('type' => 'ReturnInvoice', 'DocCode' => (int)$_POST['id_order']));
if ($commitResult['ResultCode'] == 'Warning'
|| $commitResult['ResultCode'] == 'Error'
|| $commitResult['ResultCode'] == 'Exception')
echo $this->_displayConfirmation($this->l('The following error was generated while cancelling the orders you selected. - '.
Tools::safeOutput($commitResult['Messages']['Summary'])), 'error');
else
{
$this->commitToAvalara(array('id_order' => (int)$_POST['id_order']));
echo $this->_displayConfirmation($this->l('The products you selected were cancelled.'));
}
}
}
}
public function hookUpdateOrderStatus($params)
{
if ($params['newOrderStatus']->id == (int)Configuration::get('AVALARATAX_COMMIT_ID'))
return $this->commitToAvalara($params);
elseif ($params['newOrderStatus']->id == (int)Configuration::get('AVALARATAX_CANCEL_ID'))
{
$params['CancelCode'] = 'V';
$this->cancelFromAvalara($params);
return $this->cancelFromAvalara($params);
}
elseif ($params['newOrderStatus']->id == (int)Configuration::get('AVALARATAX_POST_ID'))
{
return $this->postToAvalara($params);
}
elseif ($params['newOrderStatus']->id == (int)Configuration::get('AVALARATAX_REFUND_ID'))
{
// Commit main order
$this->commitToAvalara($params);
}
}
public function hookBackOfficeTop()
{
if (Tools::isSubmit('submitAddproduct') || Tools::isSubmit('submitAddproductAndStay'))
Db::getInstance()->Execute('REPLACE INTO '._DB_PREFIX_.'avalara_taxcodes (`id_product`, `tax_code`)
VALUES ('.(isset($_GET['id_product']) ? (int)$_GET['id_product'] : 0).', "'.pSQL(Tools::safeOutput($_POST['tax_code'])).'")');
if (isset($_GET['updateproduct']))
$productTaxCode = Db::getInstance()->ExecuteS('SELECT `tax_code`
FROM '._DB_PREFIX_.'avalara_taxcodes atc
WHERE atc.`id_product` = '.(isset($_GET['id_product']) ? (int)$_GET['id_product'] : 0));
return '
';
}
public function hookHeader()
{
global $cookie, $cart;
if (!$cart || !$cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')})
$id_address = (int)(Db::getInstance()->getValue('SELECT `id_address` FROM `'._DB_PREFIX_.'address` WHERE `id_customer` = '.(int)($cart->id_customer).' AND `deleted` = 0 ORDER BY `id`'));
else
$id_address = $cart->{Configuration::get('PS_TAX_ADDRESS_TYPE')};
$buffer = '';
return $buffer;
}
/******************************************************************/
/** Main Form Methods *********************************************/
/******************************************************************/
public function getContent()
{
global $cookie;
if (Tools::isSubmit('SubmitAvalaraTaxSettings'))
{
Configuration::updateValue('AVALARATAX_ACCOUNT_NUMBER', Tools::getValue('avalaratax_account_number'));
Configuration::updateValue('AVALARATAX_LICENSE_KEY', Tools::getValue('avalaratax_license_key'));
Configuration::updateValue('AVALARATAX_URL', Tools::getValue('avalaratax_url'));
Configuration::updateValue('AVALARATAX_COMPANY_CODE', Tools::getValue('avalaratax_company_code'));
echo $this->_displayConfirmation();
}
elseif (Tools::isSubmit('SubmitAvalaraTaxOptions'))
{
Configuration::updateValue('AVALARATAX_ADDRESS_VALIDATION', Tools::getValue('avalaratax_address_validation'));
Configuration::updateValue('AVALARATAX_TAX_CALCULATION', Tools::getValue('avalaratax_tax_calculation'));
Configuration::updateValue('AVALARATAX_TIMEOUT', (int)Tools::getValue('avalaratax_timeout'));
Configuration::updateValue('AVALARATAX_ADDRESS_NORMALIZATION', Tools::getValue('avalaratax_address_normalization'));
Configuration::updateValue('AVALARA_CACHE_MAX_LIMIT', Tools::getValue('avalara_cache_max_limit') < 1 ?
1 : Tools::getValue('avalara_cache_max_limit') > 23 ? 23 : Tools::getValue('avalara_cache_max_limit'));
echo $this->_displayConfirmation();
}
elseif (Tools::isSubmit('SubmitAvalaraTestConnection'))
$connectionTestResult = $this->_testConnection();
elseif (Tools::isSubmit('SubmitAvalaraAddressOptions'))
{
/* Validate address*/
$address = new Address();
$address->address1 = Tools::getValue('avalaratax_address_line1');
$address->address2 = Tools::getValue('avalaratax_address_line2');
$address->city = Tools::getValue('avalaratax_city');
$address->region = Tools::getValue('avalaratax_state');
$address->id_country = Tools::getValue('avalaratax_country');
$address->postcode = Tools::getValue('avalaratax_zip_code');
$normalizedAddress = $this->validateAddress($address);
if (isset($normalizedAddress['ResultCode']) && $normalizedAddress['ResultCode'] == 'Success')
{
echo $this->_displayConfirmation($this->l('The address you submitted has been validated.'));
Configuration::updateValue('AVALARATAX_ADDRESS_LINE1', $normalizedAddress['Normalized']['Line1']);
Configuration::updateValue('AVALARATAX_ADDRESS_LINE2', $normalizedAddress['Normalized']['Line2']);
Configuration::updateValue('AVALARATAX_CITY', $normalizedAddress['Normalized']['City']);
Configuration::updateValue('AVALARATAX_STATE', $normalizedAddress['Normalized']['Region']);
Configuration::updateValue('AVALARATAX_COUNTRY', $normalizedAddress['Normalized']['Country']);
Configuration::updateValue('AVALARATAX_ZIP_CODE', $normalizedAddress['Normalized']['PostalCode']);
}
else
{
echo $this->_displayConfirmation($this->l('The following error was generated while validating your address').
': - '.Tools::safeOutput($normalizedAddress['Messages']['Summary']), 'error');
Configuration::updateValue('AVALARATAX_ADDRESS_LINE1', Tools::getValue('avalaratax_address_line1'));
Configuration::updateValue('AVALARATAX_ADDRESS_LINE2', Tools::getValue('avalaratax_address_line2'));
Configuration::updateValue('AVALARATAX_CITY', Tools::getValue('avalaratax_city'));
Configuration::updateValue('AVALARATAX_STATE', Tools::getValue('avalaratax_state'));
Configuration::updateValue('AVALARATAX_ZIP_CODE', Tools::getValue('avalaratax_zip_code'));
}
}
elseif (Tools::isSubmit('SubmitAvalaraTaxClearCache'))
{
Db::getInstance()->Execute('TRUNCATE TABLE '._DB_PREFIX_.'avalara_product_cache');
Db::getInstance()->Execute('TRUNCATE TABLE '._DB_PREFIX_.'avalara_carrier_cache');
echo $this->_displayConfirmation('Cache cleared!');
}
$confValues = Configuration::getMultiple(array(
// Configuration
'AVALARATAX_ACCOUNT_NUMBER', 'AVALARATAX_LICENSE_KEY', 'AVALARATAX_URL', 'AVALARATAX_COMPANY_CODE',
// Options
'AVALARATAX_ADDRESS_VALIDATION', 'AVALARATAX_TAX_CALCULATION', 'AVALARATAX_TIMEOUT',
'AVALARATAX_ADDRESS_NORMALIZATION', 'AVALARATAX_COMMIT_ID', 'AVALARATAX_CANCEL_ID',
'AVALARATAX_REFUND_ID', 'AVALARATAX_POST_ID', 'AVALARA_CACHE_MAX_LIMIT',
// Default Address
'AVALARATAX_ADDRESS_LINE1', 'AVALARATAX_ADDRESS_LINE2', 'AVALARATAX_CITY', 'AVALARATAX_STATE',
'AVALARATAX_ZIP_CODE', 'AVALARATAX_COUNTRY'));
$stateList = array();
$stateList[] = array('id' => '0', 'name' => $this->l('Choose your state (if applicable)'), 'iso_code' => '--');
foreach (State::getStates(intval($cookie->id_lang)) as $state)
$stateList[] = array('id' => $state['id_state'], 'name' => $state['name'], 'iso_code' => $state['iso_code']);
$countryList = array();
$countryList[] = array('id' => '0', 'name' => $this->l('Choose your country'), 'iso_code' => '--');
foreach (Country::getCountries(intval($cookie->id_lang)) as $country)
$countryList[] = array('id' => $country['id_country'], 'name' => $country['name'], 'iso_code' => $country['iso_code']);
$buffer = '
'.Tools::safeOutput($this->displayName).'
'.$this->l('This module is intended to work ONLY in United States of America and Canada').'
'.$this->l('How to configure Avalara Tax Module:').'
- '.$this->l('Fill the Account Number, License Key, and Company Code fields with those provided by Avalara.').'
- '.$this->l('Specify your origin address. This is FROM where you are shipping the products (It has to be a ').''.$this->l('VALID UNITED STATES ADDRESS').')
'.$this->l('Module goal:').'
'.$this->l('This cloud-based service is the fastest, easiest, most accurate and affordable way to calculate sales and use tax; manage exemption certificates; file returns; and remit payments across North America and beyond.').'
'.$this->l('What modifications does the module do on my store?').'
- '.$this->l('Tax.php, AddressController.php, and AuthController.php will be overriden.').'
- '.$this->l('[Payment Tab -> Taxes] and [Payment Tab -> Tax Rules] configurations will be overriden for the US.').'
- '.$this->l('On product details (product in edit mode) an optional "Tax Code" field will be added allowing you to specify a valid tax code for each of your products.').'
';
return $buffer;
}
/*
** Display a custom message for settings update
** $text string Text to be displayed in the message
** $type string (confirm|warn|error) Decides what color will the message have (green|yellow)
*/
private function _displayConfirmation($text = '', $type = 'confirm')
{
if ($type == 'confirm')
$img = 'ok.gif';
elseif ($type == 'warn')
$img = 'warn2.png';
elseif ($type == 'error')
$img = 'disabled.gif';
else
die('Invalid type.');
return '