├── README.md ├── composer.json ├── modman └── src └── app ├── code └── community │ └── IntegerNet │ └── EuropeanTax │ ├── Helper │ ├── Customer │ │ └── Data.php │ └── Data.php │ ├── Model │ ├── Customer │ │ └── Observer.php │ ├── Observer.php │ └── Sales │ │ └── Quote.php │ ├── etc │ └── config.xml │ └── sql │ └── integernet_europeantax_setup │ ├── install-0.1.0.php │ ├── upgrade-0.1.0-0.2.0.php │ ├── upgrade-0.2.0-0.3.0.php │ └── upgrade-0.3.0-0.4.0.php ├── etc └── modules │ └── IntegerNet_EuropeanTax.xml └── locale └── de_DE └── IntegerNet_EuropeanTax.csv /README.md: -------------------------------------------------------------------------------- 1 | IntegerNet_EuropeanTax 2 | ===================== 3 | Tax calculation independant of customer groups, using different tax IDs for different customer addresses 4 | 5 | Facts 6 | ----- 7 | - version: 0.5.1 8 | - extension key: IntegerNet_EuropeanTax 9 | - [extension on GitHub](https://github.com/integer-net/EuropeanTax) 10 | - [direct download link](https://github.com/integer-net/EuropeanTax/archive/master.zip) 11 | 12 | Description 13 | ----------- 14 | This module creates an additional field in the customer group form called "Tax Class with valid VAT ID". It 15 | determines on the fly which tax class to use depending on a valid VAT ID being entered for the currently selected 16 | shipping address. 17 | 18 | ![Configuration Menu](https://www.integer-net.com/download/integernet-europeantax.png) 19 | 20 | Read more about the context at [https://www.integer-net.com/tax-configuration-eu-for-b2b-and-b2c-stores/](https://www.integer-net.com/tax-configuration-eu-for-b2b-and-b2c-stores/). 21 | 22 | Requirements 23 | ------------ 24 | - PHP >= 5.3.0 25 | 26 | Compatibility 27 | ------------- 28 | - Magento >= 1.6 29 | 30 | Installation Instructions 31 | ------------------------- 32 | 1. Clone the module into your document root. 33 | 2. Update the fields "Tax Class with valid VAT ID" in the customer groups 34 | 35 | Uninstallation 36 | -------------- 37 | 1. Remove all extension files from your Magento installation 38 | 39 | Support 40 | ------- 41 | If you have any issues with this extension, open an issue on [GitHub](https://github.com/integer-net/EuropeanTax/issues). 42 | 43 | Contribution 44 | ------------ 45 | Any contribution is highly appreciated. The best way to contribute code is to open a [pull request on GitHub](https://help.github.com/articles/using-pull-requests). 46 | 47 | Developer 48 | --------- 49 | Andreas von Studnitz, integer_net GmbH 50 | [http://www.integer-net.com](http://www.integer-net.com) 51 | [@integer_net](https://twitter.com/integer_net) 52 | 53 | Licence 54 | ------- 55 | [GNU General Public License 3.0](http://www.gnu.org/licenses/) 56 | 57 | Copyright 58 | --------- 59 | (c) 2016 integer_net GmbH 60 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "integer_net/european-tax", 3 | "type": "magento-module", 4 | "description": "European Tax", 5 | "homepage": "https://github.com/integer-net/EuropeanTax" 6 | } -------------------------------------------------------------------------------- /modman: -------------------------------------------------------------------------------- 1 | src/app/code/community/IntegerNet/* app/code/community/IntegerNet/ 2 | src/app/etc/modules/* app/etc/modules/ 3 | src/app/locale/de_DE/* app/locale/de_DE/ -------------------------------------------------------------------------------- /src/app/code/community/IntegerNet/EuropeanTax/Helper/Customer/Data.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class IntegerNet_EuropeanTax_Helper_Customer_Data extends Mage_Customer_Helper_Data 11 | { 12 | /** 13 | * Rewrite - Allow VAT ID to contain country code at the beginning 14 | * 15 | * @param string $countryCode 16 | * @param string $vatNumber 17 | * @param string $requesterCountryCode 18 | * @param string $requesterVatNumber 19 | * @return Varien_Object 20 | */ 21 | public function checkVatNumber($countryCode, $vatNumber, $requesterCountryCode = '', $requesterVatNumber = '') 22 | { 23 | if (substr($vatNumber, 0, 2) == $countryCode) { 24 | $vatNumber = substr($vatNumber, 2); 25 | } 26 | 27 | if ($requesterVatNumber && substr($requesterVatNumber, 0, 2) == $requesterCountryCode) { 28 | $requesterVatNumber = substr($requesterVatNumber, 2); 29 | } 30 | 31 | return parent::checkVatNumber($countryCode, $vatNumber, $requesterCountryCode, $requesterVatNumber); 32 | } 33 | } -------------------------------------------------------------------------------- /src/app/code/community/IntegerNet/EuropeanTax/Helper/Data.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class IntegerNet_EuropeanTax_Helper_Data extends Mage_Core_Helper_Abstract 11 | {} -------------------------------------------------------------------------------- /src/app/code/community/IntegerNet/EuropeanTax/Model/Customer/Observer.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class IntegerNet_EuropeanTax_Model_Customer_Observer extends Mage_Customer_Model_Observer 11 | { 12 | /** 13 | * Validate vat id if given and assign tax class to customer address 14 | * 15 | * @param Varien_Event_Observer $observer 16 | */ 17 | public function afterAddressSave($observer) 18 | { 19 | /** @var $customerAddress Mage_Customer_Model_Address */ 20 | $customerAddress = $observer->getCustomerAddress(); 21 | $customer = $customerAddress->getCustomer(); 22 | 23 | if (!Mage::helper('customer/address')->isVatValidationEnabled($customer->getStore()) 24 | || Mage::registry(self::VIV_PROCESSED_FLAG) 25 | || !$this->_canProcessAddress($customerAddress) 26 | ) { 27 | return; 28 | } 29 | 30 | try { 31 | Mage::register(self::VIV_PROCESSED_FLAG, true); 32 | 33 | /** @var $customerHelper Mage_Customer_Helper_Data */ 34 | $customerHelper = Mage::helper('customer'); 35 | 36 | if ($customerAddress->getVatId() == '' 37 | || !Mage::helper('core')->isCountryInEU($customerAddress->getCountry())) 38 | { 39 | $defaultGroupId = $customerHelper->getDefaultCustomerGroupId($customer->getStore()); 40 | 41 | if (!$customer->getDisableAutoGroupChange() && $customer->getGroupId() != $defaultGroupId) { 42 | $customerGroup = Mage::getModel('customer/group')->load($customer->getGroupId()); 43 | $customerAddress->setTaxClassId($customerGroup->getTaxClassId()); 44 | $customerAddress->save(); 45 | } 46 | } else { 47 | 48 | $result = $customerHelper->checkVatNumber( 49 | $customerAddress->getCountryId(), 50 | $customerAddress->getVatId() 51 | ); 52 | 53 | if (!$customer->getDisableAutoGroupChange()) { 54 | $customerGroup = Mage::getModel('customer/group')->load($customer->getGroupId()); 55 | if ($result->getIsValid()) { 56 | $customerAddress->setTaxClassId($customerGroup->getTaxClassIdVatId()); 57 | } else { 58 | $customerAddress->setTaxClassId($customerGroup->getTaxClassId()); 59 | } 60 | $customerAddress->save(); 61 | } 62 | 63 | if (!Mage::app()->getStore()->isAdmin()) { 64 | $validationMessage = Mage::helper('customer')->getVatValidationUserMessage($customerAddress, 65 | $customer->getDisableAutoGroupChange(), $result); 66 | 67 | if (!$validationMessage->getIsError()) { 68 | Mage::getSingleton('customer/session')->addSuccess($validationMessage->getMessage()); 69 | } else { 70 | Mage::getSingleton('customer/session')->addError($validationMessage->getMessage()); 71 | } 72 | } 73 | } 74 | } catch (Exception $e) { 75 | Mage::register(self::VIV_PROCESSED_FLAG, false, true); 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /src/app/code/community/IntegerNet/EuropeanTax/Model/Observer.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class IntegerNet_EuropeanTax_Model_Observer 11 | { 12 | /** 13 | * VAT ID validation processed flag code 14 | */ 15 | const VIV_PROCESSED_FLAG = 'viv_after_address_save_processed'; 16 | 17 | /** 18 | * Retrieve correct tax class - from quote address, if given, otherwise from default shipping address 19 | * 20 | * @param Varien_Event_Observer $observer 21 | */ 22 | public function customerLoadAfter(Varien_Event_Observer $observer) 23 | { 24 | /** @var $customer Mage_Customer_Model_Customer */ 25 | $customer = $observer->getCustomer(); 26 | if ($shippingAddress = $customer->getDefaultShippingAddress()) { 27 | 28 | if ($taxClassId = $shippingAddress->getTaxClassId()) { 29 | $customer->setTaxClassId($taxClassId); 30 | } 31 | } 32 | 33 | if ($quoteId = $this->_getSession()->getQuoteId()) { 34 | /** @var $quoteShippingAddressCollection Mage_Sales_Model_Resource_Quote_Address_Collection */ 35 | $quoteShippingAddressCollection = Mage::getResourceModel('sales/quote_address_collection'); 36 | $quoteShippingAddressCollection->addFieldToFilter('quote_id', $quoteId); 37 | $quoteShippingAddressCollection->addFieldToFilter('address_type', 'shipping'); 38 | 39 | $quoteShippingAddress = $quoteShippingAddressCollection->getFirstItem(); 40 | if ($quoteShippingAddress->getId()) { 41 | if ($taxClassId = $quoteShippingAddress->getTaxClassId()) { 42 | $customer->setTaxClassId($taxClassId); 43 | } 44 | } 45 | } 46 | } 47 | 48 | /** 49 | * Add fields to customer group form 50 | * 51 | * @param Varien_Event_Observer $observer 52 | */ 53 | public function coreBlockAbstractPrepareLayoutAfter(Varien_Event_Observer $observer) 54 | { 55 | $block = $observer->getBlock(); 56 | 57 | if ($block instanceof Mage_Adminhtml_Block_Customer_Group_Edit_Form) { 58 | 59 | $form = $block->getForm(); 60 | 61 | $fieldset = $form->getElement('base_fieldset'); 62 | 63 | if (Mage::getSingleton('adminhtml/session')->getCustomerGroupData()) { 64 | $values = Mage::getSingleton('adminhtml/session')->getCustomerGroupData(); 65 | } else { 66 | $values = Mage::registry('current_group')->getData(); 67 | } 68 | 69 | $fieldset->addField('tax_class_id_vat_id', 'select', 70 | array( 71 | 'name' => 'tax_class_id_vat_id', 72 | 'label' => Mage::helper('integernet_europeantax')->__('Tax Class with valid VAT ID'), 73 | 'title' => Mage::helper('integernet_europeantax')->__('Tax Class with valid VAT ID'), 74 | 'class' => 'required-entry', 75 | 'required' => true, 76 | 'values' => Mage::getSingleton('tax/class_source_customer')->toOptionArray(), 77 | 'value' => isset($values['tax_class_id_vat_id']) ? $values['tax_class_id_vat_id'] : null, 78 | ) 79 | ); 80 | 81 | $fieldset->addField('request_vat_id', 'select', array( 82 | 'name' => 'request_vat_id', 83 | 'label' => Mage::helper('integernet_europeantax')->__('Request VAT ID'), 84 | 'title' => Mage::helper('integernet_europeantax')->__('Request VAT ID'), 85 | 'class' => '', 86 | 'values' => Mage::getSingleton('eav/entity_attribute_source_boolean')->getAllOptions(), 87 | 'value' => isset($values['request_vat_id']) ? $values['request_vat_id'] : 0, 88 | 'required' => false, 89 | )); 90 | 91 | } 92 | } 93 | 94 | /** 95 | * Handle data of new fields in customer group when saving 96 | * 97 | * @param Varien_Event_Observer $observer 98 | */ 99 | public function customerGroupSaveBefore($observer) 100 | { 101 | /** @var Mage_Customer_Model_Group $group */ 102 | $group = $observer->getObject(); 103 | 104 | $group->setData('request_vat_id', Mage::app()->getRequest()->getParam('request_vat_id')); 105 | $group->setData('tax_class_id_vat_id', Mage::app()->getRequest()->getParam('tax_class_id_vat_id')); 106 | } 107 | 108 | /** 109 | * Validate vat id if given and assign tax class to quote shipping address 110 | * 111 | * @param Varien_Event_Observer $observer 112 | */ 113 | public function salesQuoteAddressSaveAfter(Varien_Event_Observer $observer) 114 | { 115 | /** @var Mage_Sales_Model_Quote_Address $quoteAddress */ 116 | $quoteAddress = $observer->getQuoteAddress(); 117 | 118 | if ($quoteAddress->getAddressType() != 'shipping') { 119 | return; 120 | } 121 | 122 | $customer = $quoteAddress->getQuote()->getCustomer(); 123 | 124 | if (!Mage::helper('customer/address')->isVatValidationEnabled($customer->getStore()) 125 | || Mage::registry(self::VIV_PROCESSED_FLAG) 126 | ) { 127 | return; 128 | } 129 | 130 | try { 131 | Mage::register(self::VIV_PROCESSED_FLAG, true); 132 | 133 | /** @var $customerHelper Mage_Customer_Helper_Data */ 134 | $customerHelper = Mage::helper('customer'); 135 | 136 | if ($quoteAddress->getVatId() == '' 137 | || !Mage::helper('core')->isCountryInEU($quoteAddress->getCountry())) 138 | { 139 | $defaultGroupId = $customerHelper->getDefaultCustomerGroupId($customer->getStore()); 140 | 141 | if (!$customer->getDisableAutoGroupChange() && $customer->getGroupId() != $defaultGroupId) { 142 | $customerGroup = Mage::getModel('customer/group')->load($customer->getGroupId()); 143 | $quoteAddress->setTaxClassId($customerGroup->getTaxClassId()); 144 | $quoteAddress->save(); 145 | } 146 | } else { 147 | 148 | $result = $customerHelper->checkVatNumber( 149 | $quoteAddress->getCountryId(), 150 | $quoteAddress->getVatId() 151 | ); 152 | 153 | if (!$customer->getDisableAutoGroupChange()) { 154 | $customerGroup = Mage::getModel('customer/group')->load($customer->getGroupId()); 155 | if ($result->getIsValid()) { 156 | $quoteAddress->setTaxClassId($customerGroup->getTaxClassIdVatId()); 157 | } else { 158 | $quoteAddress->setTaxClassId($customerGroup->getTaxClassId()); 159 | } 160 | $quoteAddress->save(); 161 | } 162 | 163 | if (!Mage::app()->getStore()->isAdmin()) { 164 | $validationMessage = Mage::helper('customer')->getVatValidationUserMessage($quoteAddress, 165 | $customer->getDisableAutoGroupChange(), $result); 166 | 167 | if (!$validationMessage->getIsError()) { 168 | Mage::getSingleton('customer/session')->addSuccess($validationMessage->getMessage()); 169 | } else { 170 | Mage::getSingleton('customer/session')->addError($validationMessage->getMessage()); 171 | } 172 | } 173 | } 174 | } catch (Exception $e) { 175 | Mage::register(self::VIV_PROCESSED_FLAG, false, true); 176 | } 177 | } 178 | 179 | /** 180 | * @return Mage_Checkout_Model_Session|Mage_Adminhtml_Model_Session_Quote 181 | */ 182 | protected function _getSession() 183 | { 184 | if ($this->_isAdmin()) { 185 | return Mage::getSingleton('adminhtml/session_quote'); 186 | } 187 | 188 | return Mage::getSingleton('checkout/session'); 189 | } 190 | 191 | /** 192 | * @return bool 193 | */ 194 | protected function _isAdmin() 195 | { 196 | return Mage::app()->getStore()->isAdmin() || Mage::getDesign()->getArea() == 'adminhtml'; 197 | } 198 | 199 | } -------------------------------------------------------------------------------- /src/app/code/community/IntegerNet/EuropeanTax/Model/Sales/Quote.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class IntegerNet_EuropeanTax_Model_Sales_Quote extends Mage_Sales_Model_Quote 11 | { 12 | public function getCustomerTaxClassId() 13 | { 14 | if ($this->_isAdmin()) { 15 | 16 | /** @var $customer Mage_Customer_Model_Customer */ 17 | $customer = Mage::getSingleton('adminhtml/session_quote')->getCustomer(); 18 | 19 | } else { 20 | 21 | /** @var $customer Mage_Customer_Model_Customer */ 22 | $customer = Mage::getSingleton('customer/session')->getCustomer(); 23 | } 24 | 25 | if ($taxClassId = $customer->getTaxClassId()) { 26 | $this->setCustomerTaxClassId($taxClassId); 27 | return $this->getData('customer_tax_class_id'); 28 | } 29 | 30 | return parent::getCustomerTaxClassId(); 31 | } 32 | 33 | /** 34 | * @return bool 35 | */ 36 | protected function _isAdmin() 37 | { 38 | return Mage::app()->getStore()->isAdmin() || Mage::getDesign()->getArea() == 'adminhtml'; 39 | } 40 | } -------------------------------------------------------------------------------- /src/app/code/community/IntegerNet/EuropeanTax/etc/config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 0.5.1 6 | 7 | 8 | 9 | 10 | 11 | IntegerNet_EuropeanTax_Model 12 | 13 | 14 | 15 | IntegerNet_EuropeanTax_Model_Sales_Quote 16 | 17 | 18 | 19 | 20 | 21 | IntegerNet_EuropeanTax_Helper 22 | 23 | 24 | 25 | IntegerNet_EuropeanTax_Helper_Customer_Data 26 | 27 | 28 | 29 | 30 | 31 | 32 | IntegerNet_EuropeanTax 33 | Mage_Customer_Model_Resource_Setup 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | singleton 42 | integernet_europeantax/observer 43 | customerLoadAfter 44 | 45 | 46 | 47 | 48 | 49 | 50 | integernet_europeantax/customer_observer 51 | afterAddressSave 52 | 53 | 54 | 55 | 56 | 57 | 58 | integernet_europeantax/observer 59 | salesQuoteAddressSaveAfter 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | * 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | singleton 79 | integernet_europeantax/observer 80 | coreBlockAbstractPrepareLayoutAfter 81 | 82 | 83 | 84 | 85 | 86 | 87 | singleton 88 | integernet_europeantax/observer 89 | customerGroupSaveBefore 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | IntegerNet_EuropeanTax.csv 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | disabled 112 | 113 | 114 | 115 | 116 | 117 | 118 | disabled 119 | 120 | 121 | 122 | 123 | 124 | -------------------------------------------------------------------------------- /src/app/code/community/IntegerNet/EuropeanTax/sql/integernet_europeantax_setup/install-0.1.0.php: -------------------------------------------------------------------------------- 1 | startSetup(); 6 | 7 | $installer->addAttribute('customer_address', 'tax_class_id', array( 8 | 'label' => 'Steuerklasse', 9 | 'type' => 'int', 10 | 'input' => 'select', 11 | 'source' => 'tax/class_source_customer', 12 | 'visible' => true, 13 | )); 14 | 15 | Mage::getSingleton('eav/config') 16 | ->getAttribute('customer_address', 'tax_class_id') 17 | ->setData('used_in_forms', array('adminhtml_customer_address')) 18 | ->save(); 19 | 20 | $installer->endSetup(); -------------------------------------------------------------------------------- /src/app/code/community/IntegerNet/EuropeanTax/sql/integernet_europeantax_setup/upgrade-0.1.0-0.2.0.php: -------------------------------------------------------------------------------- 1 | startSetup(); 6 | 7 | $installer->getConnection()->addColumn($this->getTable('sales_flat_quote_address'), 'tax_class_id', 'int(11)'); 8 | 9 | $installer->endSetup(); -------------------------------------------------------------------------------- /src/app/code/community/IntegerNet/EuropeanTax/sql/integernet_europeantax_setup/upgrade-0.2.0-0.3.0.php: -------------------------------------------------------------------------------- 1 | startSetup(); 6 | 7 | $installer->getConnection()->addColumn($this->getTable('customer/customer_group'), 'request_vat_id', 'int(1)'); 8 | $installer->getConnection()->addColumn($this->getTable('customer/customer_group'), 'tax_class_id_vat_id', 'int(11)'); 9 | 10 | $installer->endSetup(); -------------------------------------------------------------------------------- /src/app/code/community/IntegerNet/EuropeanTax/sql/integernet_europeantax_setup/upgrade-0.3.0-0.4.0.php: -------------------------------------------------------------------------------- 1 | startSetup(); 6 | 7 | $installer->setConfigData('customer/address/taxvat_show', ''); 8 | $installer->setConfigData('customer/create_account/auto_group_assign', 0); 9 | $installer->setConfigData('customer/create_account/tax_calculation_address_type', 'shipping'); 10 | $installer->setConfigData('customer/create_account/vat_frontend_visibility', 1); 11 | 12 | $installer->endSetup(); 13 | -------------------------------------------------------------------------------- /src/app/etc/modules/IntegerNet_EuropeanTax.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | true 6 | community 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/app/locale/de_DE/IntegerNet_EuropeanTax.csv: -------------------------------------------------------------------------------- 1 | 2 | "Tax Class with valid VAT ID","Steuerklasse bei gültiger USt.-ID" 3 | "Request VAT ID","USt.-ID anfordern" --------------------------------------------------------------------------------