commerce_vat.module in Commerce VAT 7
Defines VAT rates and Rules integration for configuring vat rules for applicability and display.
File
commerce_vat.moduleView source
<?php
/**
* @file
* Defines VAT rates and Rules integration for configuring vat rules for
* applicability and display.
*/
/**
* Implements hook_menu().
*/
function commerce_vat_menu() {
$items['admin/commerce/config/vat'] = array(
'title' => 'VAT Settings',
'description' => 'Select the calculation direction for VAT.',
'page callback' => 'drupal_get_form',
'page arguments' => array(
'commerce_vat_settings_form',
),
'access arguments' => array(
'administer vat',
),
'type' => MENU_NORMAL_ITEM,
'file' => 'includes/commerce_vat.admin.inc',
);
return $items;
}
/**
* Implements hook_permission().
*/
function commerce_vat_permission() {
return array(
'administer vat' => array(
'title' => t('Administer VAT'),
'restrict access' => TRUE,
),
);
}
/**
* Implements hook_hook_info().
*/
function commerce_vat_hook_info() {
$hooks = array(
'commerce_vat_country_info' => array(
'group' => 'commerce',
),
'commerce_vat_country_info_alter' => array(
'group' => 'commerce',
),
'commerce_vat_rate_info' => array(
'group' => 'commerce',
),
'commerce_vat_rate_info_alter' => array(
'group' => 'commerce',
),
);
return $hooks;
}
/**
* Implements hook_commerce_price_component_type_info().
*/
function commerce_vat_commerce_price_component_type_info() {
$components = array();
// Add a price component type for each vat rate that specifies it.
foreach (commerce_vat_rates() as $name => $vat_info) {
foreach ($vat_info['rates'] as $rate_info) {
if ($vat_info['price_component']) {
$components[$vat_info['price_component'] . '|' . $rate_info['name']] = array(
'title' => $vat_info['title'] . ' at ' . $rate_info['rate'],
'display_title' => $rate_info['rate'] * 100 . '% ' . t('VAT'),
'vat_rate' => $name,
'weight' => 50,
);
}
}
}
return $components;
}
/**
* Returns an array of vat country objects keyed by name.
*/
function commerce_vat_countries() {
// First check the static cache for a vat rates array.
$vat_countries =& drupal_static(__FUNCTION__);
// If it did not exist, fetch the types now.
if (!isset($vat_countries)) {
// Necessary for country_get_list().
require_once DRUPAL_ROOT . '/includes/locale.inc';
$countries = country_get_list();
$vat_countries = array();
// Find vat rates defined by hook_commerce_vat_rate_info().
foreach (module_implements('commerce_vat_country_info') as $module) {
foreach (module_invoke($module, 'commerce_vat_country_info') as $iso2 => $vat_country) {
// Initialize vat rate properties if necessary.
$defaults = array(
'title' => $countries[drupal_strtoupper($iso2)],
'iso2' => drupal_strtoupper($iso2),
'rules_component_profile' => 'commerce_vat_profile_address_' . drupal_strtolower($iso2),
'rules_component_place' => 'commerce_vat_place_of_supply_' . drupal_strtolower($iso2),
'rules_component' => 'commerce_vat_' . drupal_strtolower($iso2),
'default_profile_rules_component' => TRUE,
'default_place_rules_component' => TRUE,
'default_rules_component' => TRUE,
'default_field' => TRUE,
'module' => $module,
);
$vat_countries[$iso2] = array_merge($defaults, $vat_country);
}
}
// Last allow the info to be altered by other modules.
drupal_alter('commerce_vat_country_info', $vat_countries);
}
return $vat_countries;
}
/**
* Resets the cached list of vat rates.
*/
function commerce_vat_countries_reset() {
drupal_static_reset('commerce_vat_countries');
}
/**
* Returns an array of vat rate objects keyed by name.
*/
function commerce_vat_rates() {
// First check the static cache for a vat rates array.
$vat_rates =& drupal_static(__FUNCTION__);
// If it did not exist, fetch the types now.
if (!isset($vat_rates)) {
$vat_rates = array();
// Find vat rates defined by hook_commerce_vat_rate_info().
foreach (module_implements('commerce_vat_rate_info') as $module) {
foreach (module_invoke($module, 'commerce_vat_rate_info') as $name => $vat_rate) {
// Initialize vat rate properties if necessary.
$defaults = array(
'title' => $name,
'description' => '',
'rates' => array(),
'country' => '',
'rules_component' => 'commerce_vat_rate_' . $name,
'default_rules_component' => TRUE,
'price_component' => 'vat|' . $name,
'calculation_callback' => 'commerce_vat_rate_calculate',
'module' => $module,
);
$vat_rates[$name] = array_merge($defaults, $vat_rate);
}
}
// Last allow the info to be altered by other modules.
drupal_alter('commerce_vat_rate_info', $vat_rates);
}
return $vat_rates;
}
/**
* Resets the cached list of vat rates.
*/
function commerce_vat_rates_reset() {
drupal_static_reset('commerce_vat_rates');
}
/**
* Returns vat rates for a country.
*
* @param $iso2
* The ISO2 code of the country to load.
*
* @return
* The specified vat rate object or FALSE if it did not exist.
*/
function commerce_vat_country_rates($iso2) {
$vat_rates = commerce_vat_rates();
$country_rates = array();
foreach ($vat_rates as $name => $vat_rate) {
if ($vat_rate['country'] == $iso2) {
$country_rates[$name] = $vat_rate;
}
}
return count($country_rates) > 0 ? $country_rates : FALSE;
}
/**
* Returns a single vat rate object.
*
* @param $name
* The name of the vat rate to return.
*
* @return
* The specified vat rate object or FALSE if it did not exist.
*/
function commerce_vat_rate_load($name) {
$vat_rates = commerce_vat_rates();
return empty($vat_rates[$name]) ? FALSE : $vat_rates[$name];
}
/**
* Returns the titles of every vat rate keyed by name.
*/
function commerce_vat_rate_titles() {
$titles = array();
foreach (commerce_vat_rates() as $name => $vat_rate) {
$titles[$name] = $vat_rate['title'];
}
return $titles;
}
/**
* Calculates the Country.
*
* @param $vat_type
* The vat type object whose rates should be calculated.
* @param $line_item
* The line item to which the vates should be applied.
*/
function commerce_vat_calculate_place_of_supply($line_item) {
// Prepare an array of rules components to load.
$component_names = array();
// Loop over each vat rate in search of matching components.
foreach (commerce_vat_countries() as $name => $vat_country) {
$country_rates = commerce_vat_country_rates($vat_country['iso2']);
// If the current rate matches the type and specifies a default component...
if (!empty($vat_country['rules_component']) && !empty($country_rates)) {
$component_names[] = $vat_country['rules_component'];
}
}
// Load and invoke the vat country rules components.
if (!empty($component_names)) {
foreach (rules_config_load_multiple($component_names) as $component_name => $component) {
rules_invoke_component($component_name, $line_item);
}
}
}
/**
* Calculates vates of a particular type by invoking any components that match
* the vat type.
*
* @param $vat_type
* The vat type object whose rates should be calculated.
* @param $line_item
* The line item to which the vates should be applied.
*/
function commerce_vat_calculate_rates($line_item, $supply_iso2) {
// Prepare an array of rules components to load.
$component_names = array();
// Loop over each vat rate in search of matching components.
foreach (commerce_vat_rates() as $name => $vat_rate) {
// If the current rate matches the type and specifies a default component...
if (!empty($vat_rate['rules_component']) && $vat_rate['country'] == $supply_iso2) {
$component_names[] = $vat_rate['rules_component'];
}
}
// Load and invoke the vat rules components.
if (!empty($component_names)) {
foreach (rules_config_load_multiple($component_names) as $component_name => $component) {
rules_invoke_component($component_name, $line_item);
}
}
}
/**
* Applies a vat rate to the unit price of a line item.
*
* @param $vat_rate
* The vat rate to apply to the line item.
* @param $line_item
* The line item whose unit price will be modified to include the vat.
* @param int|null $amount
* Only for proportional VAT calculation: The unit price amount to calculate the vat for.
* Defaults to unit price amount of line item.
*
* @return
* A price array representing the vat applied to the line item or FALSE if
* none was applied.
*/
function commerce_vat_rate_apply($vat_rate, $line_item, $amount = NULL) {
$wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
if ($wrapper->order->created
->value()) {
$order_date = $wrapper->order->created
->value();
}
else {
$order_date = time();
}
foreach ($vat_rate['rates'] as $rate) {
if (strtotime($rate['start']) < $order_date) {
$rate_info = $rate;
break;
}
}
// If a valid rate is specified...
if (isset($rate_info['rate']) && is_numeric($rate_info['rate'])) {
// Don't apply vat if the unit price has a NULL amount.
if (is_null($wrapper->commerce_unit_price
->value())) {
return;
}
// Invoke the vat rate's calculation callback and apply the returned vat
// price to the line item.
if ($vat_price = $vat_rate['calculation_callback']($vat_rate, $rate_info, $wrapper, $amount)) {
// Add the vat to the unit price's data array along with a display inclusive
// property used to track whether or not the vat is included in the price.
$included = FALSE;
$direction = variable_get('commerce_vat_direction', 'forward');
if ($direction == 'reverse') {
$data = $wrapper->commerce_unit_price->data
->value();
$amount = $wrapper->commerce_unit_price->amount
->value();
// For 0 amount line items the VAT is also 0.
if ($amount == 0) {
return;
}
// Reduce components proportionally.
$amount_to_reduce_total = $vat_price['amount'];
$amount_to_reduce_left = $vat_price['amount'];
$nonzero_components = array();
foreach ($data['components'] as &$component) {
// Do not apply vat on vat components.
if (strpos($component['name'], 'vat') === 0) {
continue;
}
if ($component['price']['amount']) {
$nonzero_components[] =& $component;
}
$amount_to_reduce_current = commerce_vat_rate_round_amount($component['price']['amount'] / $amount * $amount_to_reduce_total, $vat_price['currency_code']);
$component['price']['amount'] -= $amount_to_reduce_current;
$amount_to_reduce_left -= $amount_to_reduce_current;
}
// Change components only if there was a nonzero-one.
if (!empty($nonzero_components)) {
// Remaining rounding difference deliberately reduces first nonzero component.
$nonzero_components[0]['price']['amount'] -= $amount_to_reduce_left;
$wrapper->commerce_unit_price->data = $data;
$included = TRUE;
}
}
else {
// Include the vat amount in the displayed unit price.
$wrapper->commerce_unit_price->amount = $wrapper->commerce_unit_price->amount
->value() + $vat_price['amount'];
$included = TRUE;
}
// Update the data array with the vat component.
$wrapper->commerce_unit_price->data = commerce_price_component_add($wrapper->commerce_unit_price
->value(), $vat_rate['price_component'] . '|' . $rate_info['name'], $vat_price, $included);
return $vat_price;
}
}
return FALSE;
}
/**
* Calculates a price array for the vat on the unit price of a line item.
*
* @param $vat_rate
* The vat rate array for the vat to calculate.
* @param $rate_info
* The applicable vat rate which might depend on order date.
* @param $line_item_wrapper
* An entity_metadata_wrapper() for the line item whose unit price should be
* used in the vat calculation.
* @param int|null $amount
* Only for proportional VAT calculation: The unit price amount to calculate the vat for.
* Defaults to unit price amount of line item.
*
* @return array|bool The vat price array or FALSE if the vat is already applied.
*/
function commerce_vat_rate_calculate($vat_rate, $rate_info, $line_item_wrapper, $amount = NULL) {
// By default, do not duplicate a vat that's already on the line item.
if (!is_null($line_item_wrapper->commerce_unit_price
->value()) && !commerce_price_component_load($line_item_wrapper->commerce_unit_price
->value(), $vat_rate['price_component'])) {
// If not given, calculate the vat amount.
if (!isset($amount)) {
$amount = $line_item_wrapper->commerce_unit_price->amount
->value();
}
$data = $line_item_wrapper->commerce_unit_price->data
->value();
$direction = variable_get('commerce_vat_direction', 'forward');
if ($direction == 'reverse') {
$vat_amount = $amount - $amount / (1 + $rate_info['rate']);
}
else {
if (isset($data['rounded_up']) && $data['rounded_up']) {
$amount = $amount - 0.5;
}
$vat_amount = $amount * $rate_info['rate'];
}
$currency_code = $line_item_wrapper->commerce_unit_price->currency_code
->value();
return array(
'amount' => commerce_vat_rate_round_amount($vat_amount, $currency_code),
'currency_code' => $currency_code,
'data' => array(
'vat_rate' => $vat_rate,
'vat_rate_info' => $rate_info,
),
);
}
return FALSE;
}
/**
* Rounds a VAT amount for the specified currency.
*
* @param $amount
* The VAT amount to round.
* @param $currency_code
* The currency code.
*
* @return
* The rounded VAT amount.
*/
function commerce_vat_rate_round_amount($amount, $currency_code) {
$currency = commerce_currency_load($currency_code);
return commerce_currency_round($amount, $currency);
}
/**
* Implements hook_field_widget_form_alter().
*
* Alter price widgets on the product form to have vat inclusive price entry.
* This hook was added in Drupal 7.8, so entering prices including VAT will
* require at least that version.
*/
function commerce_vat_field_widget_form_alter(&$element, &$form_state, $context) {
// Act on widgets for fields of type commerce_price on commerce_products.
$direction = variable_get('commerce_vat_direction', 'forward');
if ($context['field']['type'] == 'commerce_price' && $context['instance']['entity_type'] == 'commerce_product' && $direction == 'forward') {
// Build an array of tax types that are display inclusive.
$element['rounded_up'] = array(
'#type' => 'checkbox',
'#title' => t('Rounded Up'),
'#description' => t('This price has been rounded up, calculate VAT on 0.005'),
'#default_value' => isset($element['data']['#default_value']['rounded_up']) ? $element['data']['#default_value']['rounded_up'] : FALSE,
);
$element['#element_validate'][] = 'commerce_vat_price_field_validate';
}
}
/**
* Validate callback for the vat inclusion select list that serves to reset the
* data array based on the selected vat.
*/
function commerce_vat_price_field_validate($element, &$form_state) {
$direction = variable_get('commerce_vat_direction', 'forward');
if ($direction == 'forward') {
// Build an array of form parents to the price array.
$parents = $element['#parents'];
// Get the price array from the form state.
$price = $form_state['values'];
foreach ($parents as $parent) {
$price = $price[$parent];
}
$price['data']['rounded_up'] = $element['rounded_up']['#value'];
// Add the data array to the form state.
$parents[] = 'data';
form_set_value(array(
'#parents' => $parents,
), $price['data'], $form_state);
}
}
/**
* Implements hook_commerce_line_item_rebase_unit_price().
*/
function commerce_vat_commerce_line_item_rebase_unit_price(&$price, $old_components, $line_item) {
$inclusive_vates = array();
// Loop over the old components looking for vates that were applied.
foreach ($old_components as $key => $component) {
// Find vat components based on the vat_rate property the vat modules adds
// to vat rate component types.
if ($component_type = commerce_price_component_type_load($component['name'])) {
// Ensure the vat rate still exists.
if (!empty($component_type['vat_rate']) && ($vat_rate = commerce_vat_rate_load($component_type['vat_rate']))) {
// If this vat is displayed inclusively with product prices, add it to an
// array that we'll calculate in reverse order later.
if ($component['included']) {
$inclusive_vates[] = $vat_rate;
}
else {
// Otherwise assume we'll just have sales vates and add this one now.
// Note that this means component arrays that mix display inclusive
// and non-display inclusive vat types will not be supported; however,
// this shouldn't be possible in real world scenarios.
$vat_price = $vat_rate['calculation_callback']($vat_rate, entity_metadata_wrapper('commerce_line_item', $line_item));
// If we received a valid price array, add it as a component.
if (!empty($vat_price)) {
$price['data'] = commerce_price_component_add($price, $vat_rate['price_component'], $vat_price, FALSE);
}
}
}
}
}
// If this unit price had inclusive vates...
if (!empty($inclusive_vates)) {
// Prepare an array of vat price components.
$vat_components = array();
// See: 'commerce_vat_rate_apply()'.
$line_item_wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
if ($line_item_wrapper->order->created
->value()) {
$order_date = $line_item_wrapper->order->created
->value();
}
else {
$order_date = REQUEST_TIME;
}
// Calculate these vates in reverse order to accommodate cumulative display
// inclusive vat rates.
foreach (array_reverse($inclusive_vates) as $vat_rate) {
foreach ($vat_rate['rates'] as $rate) {
if (strtotime($rate['start']) < $order_date) {
$rate_info = $rate;
break;
}
}
// If a valid rate is specified...
if (isset($rate_info['rate']) && is_numeric($rate_info['rate'])) {
// The amount of this vat is determined by dividing the current base price
// by 1 + the vat rate expressed as a decimal (i.e. 1.1 for a 10% vat).
// The result is the base price against which the vat would have been
// applied, so the difference becomes our vat amount.
$vat_amount = $price['data']['components'][0]['price']['amount'] - $price['data']['components'][0]['price']['amount'] / (1 + $rate_info['rate']);
$vat_amount = commerce_vat_rate_round_amount($vat_amount, $price['currency_code']);
$prevat_base = $price['data']['components'][0]['price']['amount'] - $vat_amount;
// Update the base price component.
$price['data']['components'][0]['price']['amount'] = $prevat_base;
// Prepare a vat component that will be added to the price after every vat
// has been calculated.
$vat_components[$vat_rate['price_component'] . '|' . $rate_info['name']] = array(
'amount' => $vat_amount,
'currency_code' => $price['currency_code'],
'data' => array(
'vat_rate' => $vat_rate,
'vat_rate_info' => $rate_info,
),
);
}
}
// Add their components to the price in their order of appearance, though.
foreach (array_reverse($vat_components, TRUE) as $name => $vat_component) {
$price['data'] = commerce_price_component_add($price, $name, $vat_component, TRUE);
}
}
}
/**
* Returns the total amount of vat included in a price components array.
*
* @param $components
* A price's components array including potential vat components.
* @param $included
* Boolean indicating whether or not to include in the total vates that were
* included in the price amount already.
* @param $currency_code
* The currency to return the vat amount in.
*
* @return
* The consolidated vat collected for an order expressed as an integer amount.
*/
function commerce_vat_total_amount($components, $included, $currency_code) {
$component_types = commerce_vat_commerce_price_component_type_info();
$amount = 0;
// Loop over each component passed in...
foreach ($components as $component) {
// Looking for components that match one of the defined vat price components.
if (in_array($component['name'], array_keys($component_types))) {
// If the component matches the requested "included" value...
if (!$included && empty($component['included']) || $included && !empty($component['included'])) {
// Add the converted price amount to the running total.
$amount += commerce_currency_convert($component['price']['amount'], $component['price']['currency_code'], $currency_code);
}
}
}
return $amount;
}
function commerce_vat_order_rate($order) {
$component_types = commerce_vat_commerce_price_component_type_info();
$order_wrapper = entity_metadata_wrapper('commerce_order', $order);
$rates = array();
foreach ($order_wrapper->commerce_line_items as $line_item_wrapper) {
if ($line_item_wrapper->type
->value() != 'shipping') {
$commerce_total = $line_item_wrapper->commerce_total
->value();
foreach ($commerce_total['data']['components'] as $component) {
$component_name = explode('|', $component['name']);
if ($component_name[0] == 'vat') {
$rates[$component_name[1]] = $component['price']['data']['vat_rate_info']['rate'];
}
}
}
}
if (!empty($rates)) {
arsort($rates);
$rate = key($rates);
}
else {
$rate = FALSE;
}
return $rate;
}
/**
* Returns an array of vat components from a price components array.
*
* @param $components
* A price's components array including potential vat components.
*
* @return
* An array of vat price component arrays.
*/
function commerce_vat_components($components) {
$component_types = commerce_vat_commerce_price_component_type_info();
$vat_components = array();
// Loop over each component passed in...
foreach ($components as $component) {
// Looking for components that match one of the defined vat price components.
if (in_array($component['name'], array_keys($component_types))) {
// Add the component to the vat components array.
$vat_components[] = $component;
}
}
return $vat_components;
}
Functions
Name![]() |
Description |
---|---|
commerce_vat_calculate_place_of_supply | Calculates the Country. |
commerce_vat_calculate_rates | Calculates vates of a particular type by invoking any components that match the vat type. |
commerce_vat_commerce_line_item_rebase_unit_price | Implements hook_commerce_line_item_rebase_unit_price(). |
commerce_vat_commerce_price_component_type_info | Implements hook_commerce_price_component_type_info(). |
commerce_vat_components | Returns an array of vat components from a price components array. |
commerce_vat_countries | Returns an array of vat country objects keyed by name. |
commerce_vat_countries_reset | Resets the cached list of vat rates. |
commerce_vat_country_rates | Returns vat rates for a country. |
commerce_vat_field_widget_form_alter | Implements hook_field_widget_form_alter(). |
commerce_vat_hook_info | Implements hook_hook_info(). |
commerce_vat_menu | Implements hook_menu(). |
commerce_vat_order_rate | |
commerce_vat_permission | Implements hook_permission(). |
commerce_vat_price_field_validate | Validate callback for the vat inclusion select list that serves to reset the data array based on the selected vat. |
commerce_vat_rates | Returns an array of vat rate objects keyed by name. |
commerce_vat_rates_reset | Resets the cached list of vat rates. |
commerce_vat_rate_apply | Applies a vat rate to the unit price of a line item. |
commerce_vat_rate_calculate | Calculates a price array for the vat on the unit price of a line item. |
commerce_vat_rate_load | Returns a single vat rate object. |
commerce_vat_rate_round_amount | Rounds a VAT amount for the specified currency. |
commerce_vat_rate_titles | Returns the titles of every vat rate keyed by name. |
commerce_vat_total_amount | Returns the total amount of vat included in a price components array. |