You are here

commerce_vat.module in Commerce VAT 7

Defines VAT rates and Rules integration for configuring vat rules for applicability and display.

File

commerce_vat.module
View 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

Namesort descending 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.