Magento Modules: Tax By City
Tax rules can be complicated; they can be even more complicated if you need tax rules based on the city while your online store tax rules are based on postal codes. For example, districts in California have a higher sales tax rate than the state base tax rate. Magento's built-in tax rate system supports tax rates by country, state/region, and postal code. This will suffice for most stores, but the California Board Of Equalization advises against tax rate rules based on a mailing address because postal codes may cross district boundaries. Store owners need another way to calculate taxes.
Calculating taxes based on city instead of postal code can be a straightforward process as we show in this article. We will take advantage of the tax_postcode column in the tax_calculation_rate table. This is a relatively quick and simple solution for creating a new database column and modifying the Admin HTML blocks.
Directory Structure:
app/ code/ local/ Eyemagine/ Taxbycity/ etc/ config.xml Model/ Calculation.php Mysql4/ Calculation.php sql/ taxbycity_setup/ mysql-install-0.1.0.php
Configuration
File: app/code/local/Eyemagine/Taxbycity/etc/config.xml
<!--?xml version="1.0"?-->
0.1.0
Eyemagine_Taxbycity
core_setup
Eyemagine_Taxbycity_Model_Calculation
Eyemagine_Taxbycity_Model_Mysql4_Calculation
Mage_Tax_Model_Calculation and Mage_Tax_Model_Mysql4_Calculation are the one two classes we will overload. This module will affect how the tax rate is queried from the database and the criteria used to search for the tax rate.
The Setup
File: app/code/local/Eyemagine/Taxbycity/sql/taxbycity_setup/mysql-install-0.1.0.php
/**
* mysql-install-0.1.0.php
* @author EYEMAGINE
*/
/* @var $this Mage_Core_Model_Resource_Setup */
$installer = $this;
$db = $installer->getConnection();
$installer->startSetup();
$installer->run("
ALTER TABLE {$installer->getTable('tax_calculation_rate')} CHANGE `tax_postcode` `tax_postcode` VARCHAR( 32 ) NOT NULL
");
The tax_postcode column by default has a 12-character limit, which presents problems for city names such as "Rancho Santa Margarita." The setup for this module simply increases the limit to 32 characters.
The Request
File: app/code/local/Eyemagine/Taxbycity/Mode/Calculation.php
/**
* Calculation.php
* @author EYEMAGINE
*
*/
/**
* Overload the tax calculation class
* @author EYEMAGINE
*
*/
class Eyemagine_Taxbycity_Model_Calculation extends Mage_Tax_Model_Calculation
{
/**
* override the getRateRequest to add the city information to the request
* @see Mage_Tax_Model_Calculation::getRateRequest
*/
public function getRateRequest($shippingAddress = null, $billingAddress = null, $customerTaxClass = null, $store = null)
{
$address = new Varien_Object();
$session = Mage::getSingleton('customer/session');
$basedOn = Mage::getStoreConfig(Mage_Tax_Model_Config::CONFIG_XML_PATH_BASED_ON, $store);
if (($shippingAddress === false && $basedOn == 'shipping') || ($billingAddress === false && $basedOn == 'billing')) {
$basedOn = 'default';
} else {
if ((($billingAddress === false || is_null($billingAddress) || !$billingAddress->getCountryId()) && $basedOn == 'billing') || (($shippingAddress === false || is_null($shippingAddress) || !$shippingAddress->getCountryId()) && $basedOn == 'shipping')){
if (!$session->isLoggedIn()) {
$basedOn = 'default';
} else {
$defBilling = $session->getCustomer()->getDefaultBillingAddress();
$defShipping = $session->getCustomer()->getDefaultShippingAddress();
if ($basedOn == 'billing' && $defBilling && $defBilling->getCountryId()) {
$billingAddress = $defBilling;
} else if ($basedOn == 'shipping' && $defShipping && $defShipping->getCountryId()) {
$shippingAddress = $defShipping;
} else {
$basedOn = 'default';
}
}
}
}
switch ($basedOn) {
case 'billing':
$address = $billingAddress;
break;
case 'shipping':
$address = $shippingAddress;
break;
case 'origin':
$address
->setCountryId(Mage::getStoreConfig('shipping/origin/country_id', $store))
->setRegionId(Mage::getStoreConfig('shipping/origin/region_id', $store))
->setPostcode(Mage::getStoreConfig('shipping/origin/postcode', $store));
break;
case 'default':
$address
->setCountryId(Mage::getStoreConfig(Mage_Tax_Model_Config::CONFIG_XML_PATH_DEFAULT_COUNTRY, $store))
->setRegionId(Mage::getStoreConfig(Mage_Tax_Model_Config::CONFIG_XML_PATH_DEFAULT_REGION, $store))
->setPostcode(Mage::getStoreConfig(Mage_Tax_Model_Config::CONFIG_XML_PATH_DEFAULT_POSTCODE, $store));
break;
}
if (is_null($customerTaxClass) && $session->isLoggedIn()) {
$customerTaxClass = $session->getCustomer()->getTaxClassId();
} elseif (($customerTaxClass === false) || !$session->isLoggedIn()) {
$defaultCustomerGroup = Mage::getStoreConfig('customer/create_account/default_group', $store);
$customerTaxClass = Mage::getModel('customer/group')->getTaxClassId($defaultCustomerGroup);
}
$request = new Varien_Object();
$request
->setCountryId($address->getCountryId())
->setRegionId($address->getRegionId())
->setPostcode($address->getPostcode())
->setStore($store)
->setCustomerClassId($customerTaxClass)
->setCity($address->getCity());
return $request;
}
}
The calculation method, getRateRequest(), is nearly identical to the core method. We will add the city to the request object from the address object.
The Look Up
File: app/code/local/Eyemagine/Taxbycity/Mode/Mysql4/Calculation.php
/**
* Calculation.php
* @author EyeMagine
*
*/
/**
* Overload the mysql4 calculation class
* @author EyeMagine
*
*/
class Eyemagine_Taxbycity_Model_Mysql4_Calculation extends Mage_Tax_Model_Mysql4_Calculation
{
/**
* override getRates method to add city to where clause
* @see Mage_Tax_Model_Mysql4_Calculation::_getRates
*/
protected function _getRates($request)
{
$storeId = Mage::app()->getStore($request->getStore())->getId();
$select = $this->_getReadAdapter()->select();
$select
->from(array('main_table'=>$this->getMainTable()))
->where('customer_tax_class_id = ?', $request->getCustomerClassId())
->where('product_tax_class_id = ?', $request->getProductClassId());
$select->join(array('rule'=>$this->getTable('tax/tax_calculation_rule')), 'rule.tax_calculation_rule_id = main_table.tax_calculation_rule_id', array('rule.priority', 'rule.position'));
$select->join(array('rate'=>$this->getTable('tax/tax_calculation_rate')), 'rate.tax_calculation_rate_id = main_table.tax_calculation_rate_id', array('value'=>'rate.rate', 'rate.tax_country_id', 'rate.tax_region_id', 'rate.tax_postcode', 'rate.tax_calculation_rate_id', 'rate.code'));
$select
->where("rate.tax_country_id = ?", $request->getCountryId())
->where("rate.tax_region_id in ('*', '', ?)", $request->getRegionId());
$city = $request->getCity();
if (!empty($city)) {//add city only if we know we have it, default to old behavior otherwise
$select->where("LOWER(rate.tax_postcode) in ('*', '', ?)", array($request->getPostcode(),strtolower($request->getCity())));
} else {
$select->where("rate.tax_postcode in ('*', '', ?)", $request->getPostcode());
}
$select->joinLeft(array('title_table'=>$this->getTable('tax/tax_calculation_rate_title')), "rate.tax_calculation_rate_id = title_table.tax_calculation_rate_id AND title_table.store_id = '{$storeId}'", array('title'=>'IFNULL(title_table.value, rate.code)'));
$order = array('rule.priority ASC', 'rule.tax_calculation_rule_id ASC', 'rate.tax_country_id DESC', 'rate.tax_region_id DESC', 'rate.tax_postcode DESC', 'rate.rate DESC');
$select->order($order);
return $this->_getReadAdapter()->fetchAll($select);
}
}
Minor modifications to the _getRates() method, in the Mysql Calculation Model, will be required. In this method, we will ensure that the city has been set in the request and add it to the list of values to search for in the tax_postcode column.
A side affect of this is that the Estimated Shipping and Tax result will no longer be as accurate. The input for this form is a postal code, which is passed to the shipping module. Taking advantage of Magento's flexibility makes it simple to enhance its core functionality without too much code modification.
If this article was helpful or if you would like assistance with your customizations, please feel free to contact us.