commerce_tax.module in Commerce Core 7
Same filename and directory in other branches
Defines tax rates and Rules integration for configuring tax rules for applicability and display.
File
modules/tax/commerce_tax.moduleView source
<?php
/**
* @file
* Defines tax rates and Rules integration for configuring tax rules for
* applicability and display.
*/
/**
* Implements hook_hook_info().
*/
function commerce_tax_hook_info() {
$hooks = array(
'commerce_tax_type_info' => array(
'group' => 'commerce',
),
'commerce_tax_type_info_alter' => array(
'group' => 'commerce',
),
'commerce_tax_type_insert' => array(
'group' => 'commerce',
),
'commerce_tax_type_update' => array(
'group' => 'commerce',
),
'commerce_tax_type_delete' => array(
'group' => 'commerce',
),
'commerce_tax_rate_info' => array(
'group' => 'commerce',
),
'commerce_tax_rate_info_alter' => array(
'group' => 'commerce',
),
'commerce_tax_rate_insert' => array(
'group' => 'commerce',
),
'commerce_tax_rate_update' => array(
'group' => 'commerce',
),
'commerce_tax_rate_delete' => array(
'group' => 'commerce',
),
'commerce_tax_type_calculate_rates' => array(
'group' => 'commerce',
),
);
return $hooks;
}
/**
* Returns an array of tax type objects keyed by name.
*/
function commerce_tax_types() {
// First check the static cache for a tax types array.
$tax_types =& drupal_static(__FUNCTION__);
// If it did not exist, fetch the types now.
if (!isset($tax_types)) {
$tax_types = array();
// Find tax types defined by hook_commerce_tax_type_info().
foreach (module_implements('commerce_tax_type_info') as $module) {
foreach (module_invoke($module, 'commerce_tax_type_info') as $name => $tax_type) {
// Initialize tax rate properties if necessary.
$defaults = array(
'name' => $name,
'display_title' => $tax_type['title'],
'description' => '',
'display_inclusive' => FALSE,
'round_mode' => COMMERCE_ROUND_NONE,
'rule' => 'commerce_tax_type_' . $name,
'module' => $module,
);
$tax_types[$name] = array_merge($defaults, $tax_type);
}
}
// Last allow the info to be altered by other modules.
drupal_alter('commerce_tax_type_info', $tax_types);
}
return $tax_types;
}
/**
* Resets the cached list of tax types.
*/
function commerce_tax_types_reset() {
$tax_types =& drupal_static('commerce_tax_types');
$tax_types = NULL;
}
/**
* Returns a single tax type object.
*
* @param $name
* The name of the tax type to return.
*
* @return
* The specified tax type object or FALSE if it did not exist.
*/
function commerce_tax_type_load($name) {
$tax_types = commerce_tax_types();
return empty($tax_types[$name]) ? FALSE : $tax_types[$name];
}
/**
* Returns the titles of every tax type keyed by name.
*/
function commerce_tax_type_titles() {
$titles = array();
foreach (commerce_tax_types() as $name => $tax_type) {
$titles[$name] = $tax_type['title'];
}
return $titles;
}
/**
* Implements hook_commerce_price_component_type_info().
*/
function commerce_tax_commerce_price_component_type_info() {
$components = array();
// Add a price component type for each tax rate that specifies it.
foreach (commerce_tax_rates() as $name => $tax_rate) {
if ($tax_rate['price_component']) {
$components[$tax_rate['price_component']] = array(
'title' => $tax_rate['title'],
'display_title' => $tax_rate['display_title'],
'tax_rate' => $name,
);
}
}
return $components;
}
/**
* Returns an array of tax rate objects keyed by name.
*/
function commerce_tax_rates() {
// First check the static cache for a tax rates array.
$tax_rates =& drupal_static(__FUNCTION__);
// If it did not exist, fetch the types now.
if (!isset($tax_rates)) {
$tax_rates = array();
// Find tax rates defined by hook_commerce_tax_rate_info().
foreach (module_implements('commerce_tax_rate_info') as $module) {
foreach (module_invoke($module, 'commerce_tax_rate_info') as $name => $tax_rate) {
// Initialize tax rate properties if necessary.
$defaults = array(
'name' => $name,
'display_title' => $tax_rate['title'],
'description' => '',
'rate' => 0,
'type' => '',
'rules_component' => 'commerce_tax_rate_' . $name,
'default_rules_component' => TRUE,
'price_component' => 'tax|' . $name,
'calculation_callback' => 'commerce_tax_rate_calculate',
'module' => $module,
);
$tax_rates[$name] = array_merge($defaults, $tax_rate);
}
}
// Last allow the info to be altered by other modules.
drupal_alter('commerce_tax_rate_info', $tax_rates);
}
return $tax_rates;
}
/**
* Resets the cached list of tax rates.
*/
function commerce_tax_rates_reset() {
$tax_rates =& drupal_static('commerce_tax_rates');
$tax_rates = NULL;
}
/**
* Returns a single tax rate object.
*
* @param $name
* The name of the tax rate to return.
*
* @return
* The specified tax rate object or FALSE if it did not exist.
*/
function commerce_tax_rate_load($name) {
$tax_rates = commerce_tax_rates();
return empty($tax_rates[$name]) ? FALSE : $tax_rates[$name];
}
/**
* Returns the titles of every tax rate keyed by name.
*/
function commerce_tax_rate_titles() {
$titles = array();
foreach (commerce_tax_rates() as $name => $tax_rate) {
$titles[$name] = $tax_rate['title'];
}
return $titles;
}
/**
* Calculates taxes of a particular type by invoking any components that match
* the tax type.
*
* @param $tax_type
* The tax type object whose rates should be calculated.
* @param $line_item
* The line item to which the taxes should be applied.
*/
function commerce_tax_type_calculate_rates($tax_type, $line_item) {
// Prepare an array of rules components to load.
$component_names = array();
// Loop over each tax rate in search of matching components.
foreach (commerce_tax_rates() as $name => $tax_rate) {
// If the current rate matches the type and specifies a default component...
if ($tax_rate['type'] == $tax_type['name'] && !empty($tax_rate['rules_component'])) {
$component_names[] = $tax_rate['rules_component'];
}
}
// Load and invoke the tax rules components.
if (!empty($component_names)) {
foreach (rules_config_load_multiple($component_names) as $component_name => $component) {
rules_invoke_component($component_name, $line_item);
}
}
// Allow modules handling tax application on their own to apply rates of the
// current type as well.
module_invoke_all('commerce_tax_type_calculate_rates', $tax_type, $line_item);
}
/**
* Applies a tax rate to the unit price of a line item.
*
* @param $tax_rate
* The tax rate to apply to the line item.
* @param $line_item
* The line item whose unit price will be modified to include the tax.
*
* @return
* A price array representing the tax applied to the line item or FALSE if
* none was applied.
*/
function commerce_tax_rate_apply($tax_rate, $line_item) {
// If a valid rate is specified...
if (isset($tax_rate['rate']) && is_numeric($tax_rate['rate'])) {
$wrapper = entity_metadata_wrapper('commerce_line_item', $line_item);
// Don't apply tax if the unit price has a NULL amount.
if (is_null($wrapper->commerce_unit_price
->value())) {
return;
}
// Invoke the tax rate's calculation callback and apply the returned tax
// price to the line item.
if ($tax_price = $tax_rate['calculation_callback']($tax_rate, $wrapper)) {
// Add the tax to the unit price's data array along with a display inclusive
// property used to track whether or not the tax is included in the price.
$included = FALSE;
// If the rate specifies a valid tax type that is display inclusive...
if (($tax_type = commerce_tax_type_load($tax_rate['type'])) && $tax_type['display_inclusive']) {
// Include the tax amount in the displayed unit price.
$wrapper->commerce_unit_price->amount = $wrapper->commerce_unit_price->amount
->value() + $tax_price['amount'];
$included = TRUE;
}
// Update the data array with the tax component.
$wrapper->commerce_unit_price->data = commerce_price_component_add($wrapper->commerce_unit_price
->value(), $tax_rate['price_component'], $tax_price, $included);
return $tax_price;
}
}
return FALSE;
}
/**
* Calculates a price array for the tax on the unit price of a line item.
*
* @param $tax_rate
* The tax rate array for the tax to calculate.
* @param $line_item_wrapper
* An entity_metadata_wrapper() for the line item whose unit price should be
* used in the tax calculation.
*
* @return
* The tax price array or FALSE if the tax is already applied.
*/
function commerce_tax_rate_calculate($tax_rate, $line_item_wrapper) {
// By default, do not duplicate a tax 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(), $tax_rate['price_component'])) {
// Calculate the tax amount.
$amount = $line_item_wrapper->commerce_unit_price->amount
->value() * $tax_rate['rate'];
return array(
'amount' => commerce_tax_rate_round_amount($tax_rate, $amount),
'currency_code' => $line_item_wrapper->commerce_unit_price->currency_code
->value(),
'data' => array(
'tax_rate' => $tax_rate,
),
);
}
return FALSE;
}
/**
* Rounds an amount for a given tax rate.
*
* @param $tax_rate
* The tax rate array for the tax to calculate.
* @param $amount
* The amount of the tax to round.
*
* @return
* The amount rounded based on the type of tax it is for.
*/
function commerce_tax_rate_round_amount($tax_rate, $amount) {
// Round the amount according to the tax type's specification.
$tax_type = commerce_tax_type_load($tax_rate['type']);
return commerce_round($tax_type['round_mode'], $amount);
}
/**
* Implements hook_field_widget_form_alter().
*
* Alter price widgets on the product form to have tax inclusive price entry.
* This hook was added in Drupal 7.8, so entering prices including VAT will
* require at least that version.
*/
function commerce_tax_field_widget_form_alter(&$element, &$form_state, $context) {
// Act on widgets for fields of type commerce_price on commerce_products.
if ($context['field']['type'] == 'commerce_price' && $context['instance']['entity_type'] == 'commerce_product') {
// Build an array of tax types that are display inclusive.
$inclusive_types = array();
foreach (commerce_tax_types() as $name => $tax_type) {
if ($tax_type['display_inclusive']) {
$inclusive_types[$name] = $tax_type['title'];
}
}
// Build an options array of tax rates of these types.
$options = array();
foreach (commerce_tax_rates() as $name => $tax_rate) {
if (array_key_exists($tax_rate['type'], $inclusive_types)) {
$options[$inclusive_types[$tax_rate['type']]][$name] = t('Including !title', array(
'!title' => $tax_rate['title'],
));
}
}
if (!empty($options)) {
// Find the default value for the tax included element.
$default = '';
if (!empty($element['data']['#default_value']['include_tax'])) {
$default = $element['data']['#default_value']['include_tax'];
}
$element['currency_code']['#title'] = ' ';
// Note that because this is a select element, the values in the
// #options array have not been sanitized. They will be passed
// through check_plain() in form_select_options() when this form
// element is processed. If you alter the type of this element to
// radios or checkboxes, you are responsible for sanitizing the
// values of the #options array as well.
$element['include_tax'] = array(
'#type' => 'select',
'#title' => t('Include tax in this price'),
'#description' => t('Saving prices tax inclusive will bypass later calculations for the specified tax.'),
'#options' => count($options) == 1 ? reset($options) : $options,
'#default_value' => $default,
'#required' => FALSE,
'#empty_value' => '',
'#suffix' => '<div class="commerce-price-tax-included-clearfix"></div>',
'#attached' => array(
'css' => array(
drupal_get_path('module', 'commerce_tax') . '/theme/commerce_tax.theme.css',
),
),
);
}
// Append a validation handler to the price field's element validate
// array to add the included tax price component after the price has
// been converted from a decimal.
$element['#element_validate'][] = 'commerce_tax_price_field_validate';
}
}
/**
* Validate callback for the tax inclusion select list that serves to reset the
* data array based on the selected tax.
*/
function commerce_tax_price_field_validate($element, &$form_state) {
// 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];
}
// If a tax was specified...
if (!empty($element['include_tax']['#value'])) {
// Reset the components and store the tax name in the data array.
$price['data']['components'] = array();
$price['data']['include_tax'] = $element['include_tax']['#value'];
}
else {
// Otherwise reset the components array.
$price['data']['components'] = array();
unset($price['data']['include_tax']);
}
// Add the data array to the form state.
$parents[] = 'data';
form_set_value(array(
'#parents' => $parents,
), $price['data'], $form_state);
}
/**
* Implements hook_field_attach_load().
*/
function commerce_tax_field_attach_load($entity_type, $entities, $age, $options) {
// If product entities are being loaded...
if ($entity_type == 'commerce_product') {
// Loop over all the products looking for prices needing tax calculation.
foreach ($entities as $product) {
// Examine every field instance attached to this product's bundle.
foreach (field_info_instances('commerce_product', $product->type) as $field_name => $instance) {
// Load the instance's field data.
$field = field_info_field($instance['field_name']);
// If the instance is of a price field...
if ($field['type'] == 'commerce_price' && !empty($product->{$field_name})) {
// Check to see if the product has specified an included tax.
foreach ($product->{$field_name} as $langcode => $items) {
foreach ($items as $delta => $item) {
// If it specifies a tax and we can load it...
if (!empty($item['data']['include_tax']) && ($tax_rate = commerce_tax_rate_load($item['data']['include_tax']))) {
// Clean the price's components array, as we must start with a
// blank slate to rebuild the components for inclusive taxes.
$item['data']['components'] = array();
// Reverse apply the tax.
$tax_amount = $item['amount'] - $item['amount'] / (1 + $tax_rate['rate']);
$tax_amount = commerce_tax_rate_round_amount($tax_rate, $tax_amount);
// Add a base price to the data array.
$component = array(
'amount' => $item['amount'] - $tax_amount,
'currency_code' => $item['currency_code'],
'data' => array(),
);
$item['data'] = commerce_price_component_add($item, 'base_price', $component, TRUE);
// Add the tax to the data array.
$component['amount'] = $tax_amount;
$component['data']['tax_rate'] = $tax_rate;
$item['data'] = commerce_price_component_add($item, $tax_rate['price_component'], $component, TRUE);
// Set the new item on the product entity.
$product->{$field_name}[$langcode][$delta] = $item;
}
}
}
}
}
}
}
}
/**
* Implements hook_commerce_line_item_rebase_unit_price().
*/
function commerce_tax_commerce_line_item_rebase_unit_price(&$price, $old_components, $line_item) {
$inclusive_taxes = array();
// Loop over the old components looking for taxes that were applied.
foreach ($old_components as $key => $component) {
// Find tax components based on the tax_rate property the Tax modules adds
// to tax rate component types.
if ($component_type = commerce_price_component_type_load($component['name'])) {
// Ensure the tax rate still exists.
if (!empty($component_type['tax_rate']) && ($tax_rate = commerce_tax_rate_load($component_type['tax_rate']))) {
// If this tax is displayed inclusively with product prices, add it to an
// array that we'll calculate in reverse order later.
if ($component['included']) {
$inclusive_taxes[] = $tax_rate;
}
else {
// Otherwise assume we'll just have sales taxes and add this one now.
// Note that this means component arrays that mix display inclusive
// and non-display inclusive tax types will not be supported; however,
// this shouldn't be possible in real world scenarios.
$tax_price = $tax_rate['calculation_callback']($tax_rate, entity_metadata_wrapper('commerce_line_item', $line_item));
// If we received a valid price array, add it as a component.
if (!empty($tax_price)) {
$price['data'] = commerce_price_component_add($price, $tax_rate['price_component'], $tax_price, FALSE);
}
}
}
}
}
// If this unit price had inclusive taxes...
if (!empty($inclusive_taxes)) {
// We assume the first price component is the base price component that we
// will deduct the included tax amount from. If it isn't, exit without
// applying taxes because we would not be able to update the base price.
if ($price['data']['components'][0]['name'] != 'base_price') {
return;
}
// Prepare an array of tax price components.
$tax_components = array();
// Calculate these taxes in reverse order to accommodate cumulative display
// inclusive tax rates.
foreach (array_reverse($inclusive_taxes) as $tax_rate) {
// The amount of this tax is determined by dividing the current base price
// by 1 + the tax rate expressed as a decimal (i.e. 1.1 for a 10% tax).
// The result is the base price against which the tax would have been
// applied, so the difference becomes our tax amount.
$tax_amount = $price['data']['components'][0]['price']['amount'] - $price['data']['components'][0]['price']['amount'] / (1 + $tax_rate['rate']);
$tax_amount = commerce_tax_rate_round_amount($tax_rate, $tax_amount);
$pretax_base = $price['data']['components'][0]['price']['amount'] - $tax_amount;
// Update the base price component.
$price['data']['components'][0]['price']['amount'] = $pretax_base;
// Prepare a tax component that will be added to the price after every tax
// has been calculated.
$tax_components[$tax_rate['price_component']] = array(
'amount' => $tax_amount,
'currency_code' => $price['currency_code'],
'data' => array(
'tax_rate' => $tax_rate,
),
);
}
// Add their components to the price in their order of appearance, though.
foreach (array_reverse($tax_components, TRUE) as $name => $tax_component) {
$price['data'] = commerce_price_component_add($price, $name, $tax_component, TRUE);
}
}
}
/**
* Returns the total amount of tax included in a price components array.
*
* @param $components
* A price's components array including potential tax components.
* @param $included
* Boolean indicating whether or not to include in the total taxes that were
* included in the price amount already.
* @param $currency_code
* The currency to return the tax amount in.
*
* @return
* The consolidated tax collected for an order expressed as an integer amount.
*/
function commerce_tax_total_amount($components, $included, $currency_code) {
$component_types = commerce_tax_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 tax price components.
if (array_key_exists($component['name'], $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;
}
/**
* Returns an array of tax components from a price components array.
*
* @param $components
* A price's components array including potential tax components.
*
* @return
* An array of tax price component arrays.
*/
function commerce_tax_components($components) {
$component_types = commerce_tax_commerce_price_component_type_info();
$tax_components = array();
// Loop over each component passed in...
foreach ($components as $component) {
// Looking for components that match one of the defined tax price components.
if (array_key_exists($component['name'], $component_types)) {
// Add the component to the tax components array.
$tax_components[] = $component;
}
}
return $tax_components;
}
Functions
Name | Description |
---|---|
commerce_tax_commerce_line_item_rebase_unit_price | Implements hook_commerce_line_item_rebase_unit_price(). |
commerce_tax_commerce_price_component_type_info | Implements hook_commerce_price_component_type_info(). |
commerce_tax_components | Returns an array of tax components from a price components array. |
commerce_tax_field_attach_load | Implements hook_field_attach_load(). |
commerce_tax_field_widget_form_alter | Implements hook_field_widget_form_alter(). |
commerce_tax_hook_info | Implements hook_hook_info(). |
commerce_tax_price_field_validate | Validate callback for the tax inclusion select list that serves to reset the data array based on the selected tax. |
commerce_tax_rates | Returns an array of tax rate objects keyed by name. |
commerce_tax_rates_reset | Resets the cached list of tax rates. |
commerce_tax_rate_apply | Applies a tax rate to the unit price of a line item. |
commerce_tax_rate_calculate | Calculates a price array for the tax on the unit price of a line item. |
commerce_tax_rate_load | Returns a single tax rate object. |
commerce_tax_rate_round_amount | Rounds an amount for a given tax rate. |
commerce_tax_rate_titles | Returns the titles of every tax rate keyed by name. |
commerce_tax_total_amount | Returns the total amount of tax included in a price components array. |
commerce_tax_types | Returns an array of tax type objects keyed by name. |
commerce_tax_types_reset | Resets the cached list of tax types. |
commerce_tax_type_calculate_rates | Calculates taxes of a particular type by invoking any components that match the tax type. |
commerce_tax_type_load | Returns a single tax type object. |
commerce_tax_type_titles | Returns the titles of every tax type keyed by name. |