You are here

uc_taxes.module in Ubercart 6.2

Same filename and directory in other branches
  1. 5 uc_taxes/uc_taxes.module
  2. 7.3 uc_taxes/uc_taxes.module

File

uc_taxes/uc_taxes.module
View source
<?php

/**
 * @file
 * Ubercart Taxes module.
 *
 * Allows tax rules to be set up and applied to orders.
 */
require_once 'uc_taxes.ca.inc';

/******************************************************************************
 * Drupal Hooks                                                               *
 ******************************************************************************/

/**
 * Implements hook_help().
 */
function uc_taxes_help($path, $arg) {
  $output = '';
  switch ($path) {
    case 'admin/store/settings/taxes':
      return t('Add tax rates through this page and then use the <a href="!url">conditional actions interface</a> to add conditions to the taxes that limit which orders they are applied to. Especially important are the geographic area conditions for the delivery address.  Use the conditions link to jump to a particular tax rate conditions configuration page.', array(
        '!url' => url(CA_UI_PATH),
      ));
  }
  return $output;
}

/**
 * Implements hook_perm().
 */
function uc_taxes_perm() {
  return array(
    'configure taxes',
  );
}

/**
 * Implements hook_menu().
 */
function uc_taxes_menu() {
  $items = array();
  $items['admin/store/settings/taxes'] = array(
    'title' => 'Tax rates and settings',
    'description' => 'Configure the tax rates and settings.',
    'page callback' => 'uc_taxes_admin_settings',
    'access arguments' => array(
      'configure taxes',
    ),
    'type' => MENU_NORMAL_ITEM,
    'file' => 'uc_taxes.admin.inc',
  );
  $items['admin/store/settings/taxes/overview'] = array(
    'title' => 'Overview',
    'weight' => 0,
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
  $items['admin/store/settings/taxes/add'] = array(
    'title' => 'Add a tax rate',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'uc_taxes_form',
    ),
    'access arguments' => array(
      'configure taxes',
    ),
    'file' => 'uc_taxes.admin.inc',
    'weight' => 5,
    'type' => MENU_LOCAL_TASK,
  );
  $items['admin/store/settings/taxes/%/edit'] = array(
    'title' => 'Edit a tax rate',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'uc_taxes_form',
      4,
    ),
    'access arguments' => array(
      'configure taxes',
    ),
    'type' => MENU_CALLBACK,
    'file' => 'uc_taxes.admin.inc',
  );
  $items['admin/store/settings/taxes/%/clone'] = array(
    'page callback' => 'uc_taxes_clone',
    'page arguments' => array(
      4,
    ),
    'access arguments' => array(
      'configure taxes',
    ),
    'type' => MENU_CALLBACK,
    'file' => 'uc_taxes.admin.inc',
  );
  $items['admin/store/settings/taxes/%/delete'] = array(
    'title' => 'Delete tax rule',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'uc_taxes_delete_form',
      4,
    ),
    'access arguments' => array(
      'configure taxes',
    ),
    'type' => MENU_CALLBACK,
    'file' => 'uc_taxes.admin.inc',
  );
  $items['taxes/calculate'] = array(
    'page callback' => 'uc_taxes_javascript',
    'access arguments' => array(
      'access content',
    ),
    'type' => MENU_CALLBACK,
  );
  return $items;
}

/**
 * Implements hook_form_alter().
 */
function uc_taxes_form_alter(&$form, $form_state, $form_id) {
  if ($form_id == 'uc_cart_checkout_form') {
    if (isset($form['panes']['payment'])) {
      drupal_add_js(array(
        'ucTaxWeight' => variable_get('uc_li_tax_weight', 9),
        'ucURL' => array(
          'calculateTax' => url('taxes/calculate'),
        ),
      ), 'setting');
      drupal_add_js(drupal_get_path('module', 'uc_taxes') . '/uc_taxes.js');
    }
  }
  elseif ($form_id == 'uc_order_edit_form') {
    if (isset($form['quotes'])) {
      drupal_add_js(array(
        'ucURL' => array(
          'calculateTax' => url('taxes/calculate'),
        ),
      ), 'setting');
      drupal_add_js(drupal_get_path('module', 'uc_taxes') . '/uc_taxes.js');
    }
  }
}

/*******************************************************************************
 * Ubercart Hooks
 ******************************************************************************/

/**
 * Implements hook_line_item().
 */
function uc_taxes_line_item() {
  $items[] = array(
    'id' => 'tax',
    'title' => t('Tax'),
    'callback' => 'uc_line_item_tax',
    'weight' => 9,
    'stored' => TRUE,
    'default' => FALSE,
    'calculated' => TRUE,
    'display_only' => FALSE,
  );
  $items[] = array(
    'id' => 'tax_subtotal',
    'title' => t('Subtotal excluding taxes'),
    'callback' => 'uc_line_item_tax_subtotal',
    'weight' => 7,
    'stored' => FALSE,
    'calculated' => FALSE,
  );
  return $items;
}

/**
 * Implements hook_order().
 *
 * Updates and saves tax line items to the order.
 */
function uc_taxes_order($op, $arg1, $arg2) {
  switch ($op) {
    case 'save':
      $changes = array();
      $callback = _line_item_data('tax', 'callback');
      $line_items = $callback('load', $arg1);
      $context = array(
        'revision' => 'formatted',
        'type' => 'line_item',
        'subject' => array(
          'order' => $arg1,
        ),
      );
      if (is_array($arg1->line_items)) {
        foreach ($arg1->line_items as $i => $line) {
          if ($line['type'] == 'tax') {
            $delete = TRUE;
            foreach ($line_items as $id => $new_line) {
              if ($new_line['title'] == $line['title']) {
                if ($new_line['amount'] != $line['amount']) {
                  $context['subject']['line_item'] = $new_line;
                  uc_order_update_line_item($line['line_item_id'], $new_line['title'], $new_line['amount'], $new_line['data']);
                  $arg1->line_items[$i]['amount'] = $new_line['amount'];
                  $changes[] = t('Changed %title to %amount.', array(
                    '%amount' => uc_price($new_line['amount'], $context),
                    '%title' => $new_line['title'],
                  ));
                }
                unset($line_items[$id]);
                $delete = FALSE;
                break;
              }
            }
            if ($delete) {
              uc_order_delete_line_item($line['line_item_id']);
              unset($arg1->line_items[$i]);
              $changes[] = t('Removed %title.', array(
                '%title' => $line['title'],
              ));
            }
          }
        }
      }
      if (is_array($line_items)) {
        foreach ($line_items as $line) {
          uc_order_line_item_add($arg1->order_id, $line['id'], $line['title'], $line['amount'], $line['weight'], $line['data']);
          $line['type'] = 'tax';
          $arg1->line_items[] = $line;
          $context['subject']['line_item'] = $line;
          $changes[] = t('Added %amount for %title.', array(
            '%amount' => uc_price($line['amount'], $context),
            '%title' => $line['title'],
          ));
        }
      }
      if (count($changes)) {
        uc_order_log_changes($arg1->order_id, $changes);
      }
      break;
  }
}

/******************************************************************************
 * Menu Callbacks                                                             *
 ******************************************************************************/

/**
 * Handles the tax line item.
 */
function uc_line_item_tax($op, $order) {
  switch ($op) {
    case 'load':
      $lines = array();
      $taxes = uc_taxes_calculate($order);
      foreach ($taxes as $tax) {
        $lines[] = array(
          'id' => $tax->summed ? 'tax' : 'tax_included',
          'title' => $tax->name,
          'amount' => $tax->amount,
          'weight' => variable_get('uc_li_tax_weight', 9) + $tax->weight / 10,
          'data' => $tax->data,
        );
      }
      return $lines;
  }
}

/**
 * Handles the line item subtotal before taxes.
 */
function uc_line_item_tax_subtotal($op, $order) {
  $amount = 0;
  $has_taxes = FALSE;
  $different = FALSE;
  if (is_array($order->products)) {
    foreach ($order->products as $item) {
      $amount += $item->price * $item->qty;
    }
  }
  if (is_array($order->line_items)) {
    foreach ($order->line_items as $key => $line_item) {
      if ($line_item['type'] == 'subtotal') {
        continue;
      }
      if (substr($line_item['type'], 0, 3) != 'tax') {
        $amount += $line_item['amount'];
        $different = TRUE;
      }
      else {
        $has_taxes = TRUE;
      }
    }
  }
  if (isset($order->taxes) && is_array($order->taxes)) {
    $has_taxes = TRUE;
  }
  if ($different && $has_taxes) {
    switch ($op) {
      case 'cart-preview':
        drupal_add_js("if (Drupal.jsEnabled) { \$(document).ready(function() {\n          if (window.set_line_item) {\n            set_line_item('tax_subtotal', '" . t('Subtotal excluding taxes') . "', " . $amount . ", " . variable_get('uc_li_tax_subtotal_weight', 8) . ");\n          }\n        })};", 'inline');
        break;
      case 'load':
        return array(
          array(
            'id' => 'tax_subtotal',
            'title' => t('Subtotal excluding taxes'),
            'amount' => $amount,
            'weight' => variable_get('uc_li_tax_subtotal_weight', 7),
          ),
        );
    }
  }
}

/******************************************************************************
 * Module and Helper Functions
 ******************************************************************************/

/**
 * Saves a tax rate to the database.
 *
 * @param $rate
 *   The tax rate object to be saved.
 *
 * @return
 *   The saved tax rate object including the rate ID for new rates.
 */
function uc_taxes_rate_save($rate) {

  // Save it as a new rate if no ID is specified.
  if (!$rate->id) {
    drupal_write_record('uc_taxes', $rate);
  }
  else {
    drupal_write_record('uc_taxes', $rate, array(
      'id',
    ));
  }
  return $rate;
}

/**
 * Loads a tax rate or all tax rates from the database.
 *
 * @param $rate_id
 *   The ID of the specific rate to load or NULL to return all available rates.
 *
 * @return
 *   An object representing the requested tax rate or an array of all tax rates
 *   keyed by rate ID.
 */
function uc_taxes_rate_load($rate_id = NULL) {
  static $rates = array();

  // If the rates have not been cached yet...
  if (empty($rates)) {

    // Get all the rate data from the database.
    $result = db_query("SELECT * FROM {uc_taxes} ORDER BY weight");

    // Loop through each returned row.
    while ($rate = db_fetch_object($result)) {

      // Unserialize some arrays and cache the rate in a static array.
      $rate->taxed_product_types = unserialize($rate->taxed_product_types);
      $rate->taxed_line_items = unserialize($rate->taxed_line_items);
      $rates[$rate->id] = $rate;
    }
  }

  // Return a rate as specified.
  if ($rate_id) {
    return $rates[$rate_id];
  }
  else {
    return $rates;
  }
}

/**
 * Deletes a tax rate from the database.
 *
 * @param $rate_id
 *   The ID of the tax rate to delete.
 */
function uc_taxes_rate_delete($rate_id) {

  // Delete the tax rate record.
  db_query("DELETE FROM {uc_taxes} WHERE id = %d", $rate_id);

  // Delete the associated predicated if it has been saved to the database.
  ca_delete_predicate('uc_taxes_' . $rate_id);
}

/**
 * Calculates the taxes for an order based on enabled tax modules.
 *
 * @param $order
 *   The full order object for the order want to calculate taxes for.
 *
 * @return
 *   An array of taxes for the order.
 */
function uc_taxes_calculate($order) {

  // Find any taxes specified by enabled modules.
  $taxes = module_invoke_all('calculate_tax', $order);
  return $taxes;
}

/**
 * Calculates the amount and types of taxes that apply to an order.
 */
function uc_taxes_calculate_tax($order) {
  global $user;
  if (is_numeric($order)) {
    $order = uc_order_load($order);
    $account = user_load(array(
      'uid' => $order->uid,
    ));
  }
  elseif ((int) $order->uid) {
    $account = user_load(array(
      'uid' => intval($order->uid),
    ));
  }
  else {
    $account = $user;
  }
  if (!is_object($order)) {
    return array();
  }
  if (empty($order->delivery_postal_code)) {
    $order->delivery_postal_code = $order->billing_postal_code;
  }
  if (empty($order->delivery_zone)) {
    $order->delivery_zone = $order->billing_zone;
  }
  if (empty($order->delivery_country)) {
    $order->delivery_country = $order->billing_country;
  }
  $order->taxes = array();
  if (isset($order->order_status)) {
    $state = uc_order_status_data($order->order_status, 'state');
    $use_same_rates = in_array($state, array(
      'payment_received',
      'completed',
    ));
  }
  else {
    $use_same_rates = FALSE;
  }
  $arguments = array(
    'order' => array(
      '#entity' => 'uc_order',
      '#title' => t('Order'),
      '#data' => $order,
    ),
    'tax' => array(
      '#entity' => 'tax',
      '#title' => t('Tax rule'),
    ),
    'account' => array(
      '#entity' => 'user',
      '#title' => t('User'),
      '#data' => $account,
    ),
  );
  $predicates = ca_load_trigger_predicates('calculate_taxes');
  foreach (uc_taxes_rate_load() as $tax_rate) {
    $tax = clone $tax_rate;
    if ($use_same_rates) {
      foreach ((array) $order->line_items as $old_line) {
        if ($old_line['type'] == 'tax' && $old_line['data']['tax_id'] == $tax->id) {
          $tax->rate = $old_line['data']['tax_rate'];
          break;
        }
      }
    }
    $arguments['tax']['#data'] = $tax;
    if (ca_evaluate_conditions($predicates['uc_taxes_' . $tax->id], $arguments)) {
      $line_item = uc_taxes_action_apply_tax($order, $tax);
      if ($line_item) {
        $order->taxes[$line_item->id] = $line_item;
      }
    }
  }
  return $order->taxes;
}

/**
 * AJAX callback for order preview.
 *
 * Calculates tax amounts for an order in the payment checkout pane.
 */
function uc_taxes_javascript() {
  $order = $_POST['order'];
  if ($order = unserialize(rawurldecode($order))) {
    $taxes = module_invoke_all('calculate_tax', $order);
    $callback = _line_item_data('tax_subtotal', 'callback');
    if (function_exists($callback)) {
      $subtotal = $callback('load', $order);
      if (is_array($subtotal) && !empty($taxes)) {
        $taxes['subtotal'] = (object) array(
          'id' => 'subtotal',
          'name' => $subtotal[0]['title'],
          'amount' => $subtotal[0]['amount'],
          'weight' => -10,
          'summed' => 0,
        );
      }
    }
  }
  drupal_json((array) $taxes);
}

/**
 * Calculates tax for a single product.
 */
function uc_taxes_apply_item_tax($item, $tax) {
  $node = node_load($item->nid);

  // Special handling for manually added "Blank line" products.
  if (!$node) {
    $node = new stdClass();
    $node->type = 'blank-line';
    $node->shippable = $item->weight > 0;
  }

  // Tax products if they are of a taxed type and if it is shippable if
  // the tax only applies to shippable products.
  if (in_array($node->type, $tax->taxed_product_types) && ($tax->shippable == 0 || $node->shippable == 1)) {
    $context = array(
      'revision' => 'altered',
      'type' => 'cart_item',
      'subject' => array(
        'cart_item' => $item,
        'node' => $item->nid ? $node : FALSE,
      ),
    );
    $price_info = array(
      'price' => $item->price,
      'qty' => $item->qty,
    );
    return uc_price($price_info, $context);
  }
}

/**
 * Applies taxes to an order.
 *
 * @param $order
 *   The order object being considered.
 * @param $tax
 *   The tax rule calculating the amount.
 *
 * @return
 *   The line item array representing the amount of tax.
 */
function uc_taxes_apply_tax($order, $tax) {
  $amount = 0;
  $taxable_amount = 0;
  if (is_array($order->products)) {
    foreach ($order->products as $item) {
      $taxable_amount += uc_taxes_apply_item_tax($item, $tax);
    }
  }
  $taxed_line_items = $tax->taxed_line_items;
  if (is_array($order->line_items) && is_array($taxed_line_items)) {
    foreach ($order->line_items as $key => $line_item) {
      if ($line_item['type'] == 'tax') {

        // Don't tax old taxes.
        continue;
      }
      if (in_array($line_item['type'], $taxed_line_items)) {
        $callback = _line_item_data($line_item['type'], 'tax_adjustment');
        if (isset($callback) && function_exists($callback)) {
          $taxable_amount += $callback($line_item['amount'], $order, $tax) / $tax->rate;
        }
        else {
          $taxable_amount += $line_item['amount'];
        }
      }
    }
  }
  if (isset($taxed_line_items['tax'])) {

    // Tax taxes that were just calculated.
    foreach ($order->taxes as $other_tax) {
      $taxable_amount += $other_tax->amount;
    }
  }
  $amount = $taxable_amount * $tax->rate;
  if ($amount) {
    $line_item = (object) array(
      'id' => $tax->id,
      'name' => $tax->name,
      'amount' => $amount,
      'weight' => $tax->weight,
      'summed' => 1,
    );
    $line_item->data = array(
      'tax_id' => $tax->id,
      'tax_rate' => $tax->rate,
      'taxable_amount' => $taxable_amount,
      'tax_jurisdiction' => $tax->name,
    );
    return $line_item;
  }
}

Functions

Namesort descending Description
uc_line_item_tax Handles the tax line item.
uc_line_item_tax_subtotal Handles the line item subtotal before taxes.
uc_taxes_apply_item_tax Calculates tax for a single product.
uc_taxes_apply_tax Applies taxes to an order.
uc_taxes_calculate Calculates the taxes for an order based on enabled tax modules.
uc_taxes_calculate_tax Calculates the amount and types of taxes that apply to an order.
uc_taxes_form_alter Implements hook_form_alter().
uc_taxes_help Implements hook_help().
uc_taxes_javascript AJAX callback for order preview.
uc_taxes_line_item Implements hook_line_item().
uc_taxes_menu Implements hook_menu().
uc_taxes_order Implements hook_order().
uc_taxes_perm Implements hook_perm().
uc_taxes_rate_delete Deletes a tax rate from the database.
uc_taxes_rate_load Loads a tax rate or all tax rates from the database.
uc_taxes_rate_save Saves a tax rate to the database.