You are here

uc_taxes.module in Ubercart 5

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

File

uc_taxes/uc_taxes.module
View source
<?php

/**
 * @file
 * Übercart Taxes module.
 *
 * Allows tax rules to be set up and applied to orders.
 *
 * Coded by Lyle Mantooth
 */
require_once 'uc_taxes_workflow.inc';

/******************************************************************************
 * Drupal Hooks                                                               *
 ******************************************************************************/
function uc_taxes_help($section) {
  switch ($section) {
    case 'admin/store/settings/taxes':
      return t('Add tax rates through this page and then use <a href="!url">Workflow-ng</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 Workflow-ng configuration page.', array(
        '!url' => _workflow_ng_ui_path(TRUE),
      ));
  }
}

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

/**
 * Implementation of hook_menu().
 */
function uc_taxes_menu($may_cache) {
  $items = array();
  if ($may_cache) {
    $items[] = array(
      'path' => 'admin/store/settings/taxes',
      'title' => t('Tax rates and settings'),
      'access' => user_access('configure taxes'),
      'callback' => 'uc_taxes_admin_settings',
      'type' => MENU_NORMAL_ITEM,
    );
    $items[] = array(
      'path' => 'admin/store/settings/taxes/edit',
      'title' => t('Edit tax rule'),
      'access' => user_access('configure taxes'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'uc_taxes_form',
      ),
      'type' => MENU_CALLBACK,
    );
    $items[] = array(
      'path' => 'admin/store/settings/taxes/copy',
      'access' => user_access('configure taxes'),
      'callback' => 'uc_taxes_copy',
      'type' => MENU_CALLBACK,
    );
    $items[] = array(
      'path' => 'admin/store/settings/taxes/delete',
      'title' => t('Delete tax rule'),
      'access' => user_access('configure taxes'),
      'callback' => 'drupal_get_form',
      'callback arguments' => array(
        'uc_taxes_delete',
      ),
      'type' => MENU_CALLBACK,
    );
    $items[] = array(
      'path' => 'taxes/calculate',
      'access' => TRUE,
      'callback' => 'uc_taxes_javascript',
      'type' => MENU_CALLBACK,
    );
  }
  return $items;
}
function uc_taxes_form_alter($form_id, &$form) {
  if ($form_id == 'uc_cart_checkout_form') {
    if (isset($form['panes']['payment'])) {
      uc_add_js('var tax_weight = ' . variable_get('uc_li_tax_weight', 9), 'inline');
      uc_add_js(drupal_get_path('module', 'uc_taxes') . '/uc_taxes.js');
    }
  }
  else {
    if ($form_id == 'uc_order_edit_form') {
      if (isset($form['quotes'])) {
        uc_add_js(drupal_get_path('module', 'uc_taxes') . '/uc_taxes.js');
      }
    }
  }
}

/*******************************************************************************
 * Übercart Hooks
 ******************************************************************************/

/**
 * Implementation of Übercart's 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;
}

/**
 * Update and save tax line items to the order.
 */
function uc_taxes_order($op, $arg1, $arg2) {
  switch ($op) {
    case 'save':
      $changes = array();
      $line_items = uc_line_item_tax('load', $arg1);

      //$arg1->line_items = uc_order_load_line_items($arg1, true);
      if (is_array($arg1->line_items)) {

        //drupal_set_message('<pre>'. var_export($arg1->line_items, true) .'</pre>');
        foreach ($arg1->line_items as $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']) {
                  uc_order_update_line_item($line['line_item_id'], $new_line['title'], $new_line['amount'], $new_line['data']);
                  $changes[] = t('Changed %title to %amount.', array(
                    '%amount' => uc_currency_format($new_line['amount']),
                    '%title' => $new_line['title'],
                  ));
                }
                unset($line_items[$id]);
                $delete = false;
                break;
              }
            }
            if ($delete) {
              uc_order_delete_line_item($line['line_item_id']);
              $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']);
          $changes[] = t('Added %amount for %title.', array(
            '%amount' => uc_currency_format($line['amount']),
            '%title' => $line['title'],
          ));
        }
      }
      if (count($changes)) {
        uc_order_log_changes($arg1->order_id, $changes);
      }
      break;
  }
}

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

/**
 * Display a list of tax rates.
 */
function uc_taxes_admin_settings() {
  $header = array(
    t('Name'),
    t('Rate'),
    t('Taxed product types'),
    t('Tax only shippable?'),
    t('Taxed line items'),
    t('Weight'),
    array(
      'data' => t('Actions'),
      'colspan' => 3,
    ),
  );
  $result = db_query("SELECT * FROM {uc_taxes} ORDER BY weight");
  while ($rule = db_fetch_object($result)) {
    $rule->taxed_product_types = (array) unserialize($rule->taxed_product_types);
    $rule->taxed_line_items = (array) unserialize($rule->taxed_line_items);
    $rows[] = array(
      $rule->name,
      $rule->rate * 100 . '%',
      implode(', ', $rule->taxed_product_types),
      $rule->shippable ? t('Yes') : '',
      implode(', ', $rule->taxed_line_items),
      $rule->weight,
      l(t('edit'), 'admin/store/settings/taxes/edit/' . $rule->id),
      l(t('copy'), 'admin/store/settings/taxes/copy/' . $rule->id),
      l(t('delete'), 'admin/store/settings/taxes/delete/' . $rule->id),
    );
  }
  $output = theme('table', $header, $rows);
  $output .= l('Make a new tax rule.', 'admin/store/settings/taxes/edit');
  return $output;
}

/**
 * Create or edit a tax rule.
 *
 * @ingroup forms
 * @see uc_taxes_form_submit
 */
function uc_taxes_form($tax_id = 0) {
  $form = array();
  if ($tax_id != 0) {
    $form_values = db_fetch_array(db_query("SELECT * FROM {uc_taxes} WHERE id = %d", $tax_id));
    $form_values['taxed_product_types'] = array_values(unserialize($form_values['taxed_product_types']));
    $form_values['taxed_line_items'] = array_values(unserialize($form_values['taxed_line_items']));
  }
  $form['id'] = array(
    '#type' => 'hidden',
    '#value' => $form_values['id'],
  );
  $form['name'] = array(
    '#type' => 'textfield',
    '#title' => t('Name'),
    '#default_value' => isset($form_values['name']) ? $form_values['name'] : '',
    '#required' => true,
  );
  $form['rate'] = array(
    '#type' => 'textfield',
    '#title' => t('Rate'),
    '#description' => t('The tax rate as a percent or decimal. Examples: 6%, .06'),
    '#default_value' => isset($form_values['rate']) ? $form_values['rate'] : '',
    '#size' => 15,
    '#required' => true,
  );
  $shippable_options = array(
    '0' => t('Tax all products regardless of shipping status'),
    1 => t('Tax only shippable products'),
  );
  $form['shippable'] = array(
    '#type' => 'radios',
    '#title' => t('Tax products based on shipping status?'),
    '#options' => $shippable_options,
    '#default_value' => $form_values['shippable'],
  );
  $types = array();
  $node_types = module_invoke_all('node_info');
  $product_types = module_invoke_all('product_types');
  foreach ($product_types as $type) {
    $types[$type] = $node_types[$type]['name'];
  }
  $form['taxed_product_types'] = array(
    '#type' => 'checkboxes',
    '#title' => t('Taxed product types'),
    '#multiple' => true,
    '#options' => $types,
    '#default_value' => $form_values['taxed_product_types'],
    '#description' => t('Apply taxes to specific product types.'),
  );
  $options = array();
  foreach (_line_item_list() as $line_item) {
    if (!in_array($line_item['id'], array(
      'subtotal',
      'tax_subtotal',
      'total',
    ))) {
      $options[$line_item['id']] = $line_item['title'];
    }
  }
  $form['taxed_line_items'] = array(
    '#type' => 'checkboxes',
    '#title' => t('Taxed line items'),
    '#multiple' => true,
    '#options' => $options,
    '#default_value' => $form_values['taxed_line_items'],
    '#description' => t('Adds the checked line item types to the total before applying this tax.'),
  );
  $form['weight'] = array(
    '#type' => 'weight',
    '#title' => t('Weight'),
    '#default_value' => isset($form_values['weight']) ? $form_values['weight'] : 0,
    '#description' => t('Lighter taxes are applied to an order first. This value is unimportant if there are no taxes on tax line items.'),
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Submit'),
  );
  return $form;
}

/**
 * Submit handler for uc_taxes_form().
 */
function uc_taxes_form_submit($form_id, $form_values) {
  if ($form_values['op'] == t('Submit')) {
    if (strpos($form_values['rate'], '%')) {
      $rate = floatval($form_values['rate']) / 100;
    }
    else {
      $rate = floatval($form_values['rate']);
    }
    if ($form_values['id'] != '') {
      db_query("UPDATE {uc_taxes} SET name = '%s', rate = %f, taxed_product_types = '%s', taxed_line_items = '%s', weight = %d, shippable = %d WHERE id = %d", $form_values['name'], $rate, serialize(array_filter($form_values['taxed_product_types'])), serialize(array_filter($form_values['taxed_line_items'])), $form_values['weight'], $form_values['shippable'], $form_values['id']);
    }
    else {
      db_query("INSERT INTO {uc_taxes} (id, name, rate, taxed_product_types, taxed_line_items, weight, shippable) VALUES (%d, '%s', %f, '%s', '%s', %d, %d)", db_next_id('{uc_taxes}_id'), $form_values['name'], $rate, serialize(array_filter($form_values['taxed_product_types'])), serialize(array_filter($form_values['taxed_line_items'])), $form_values['weight'], $form_values['shippable']);
    }
    drupal_set_message(t('%name settings have been saved.', array(
      '%name' => $form_values['name'],
    )));
  }
  return 'admin/store/settings/taxes';
}

/**
 * Copy a tax rule.
 */
function uc_taxes_copy($tax_id) {
  $rule = db_fetch_array(db_query("SELECT * FROM {uc_taxes} WHERE id = %d", $tax_id));
  db_query("INSERT INTO {uc_taxes} (id, name, rate, taxed_product_types, taxed_line_items, weight, shippable) VALUES (%d, '%s', %f, '%s', '%s', %d, %d)", db_next_id('{uc_taxes}_id'), t('Copy of @rule', array(
    '%rule' => $rule['name'],
  )), $rule['rate'], $rule['taxed_product_types'], $rule['taxed_line_items'], $rule['weight'], $rule['shippable']);
  drupal_goto('admin/store/settings/taxes');
}

/**
 * Delete a tax rule.
 *
 * @ingroup forms
 * @see uc_taxes_delete_submit
 */
function uc_taxes_delete($tax_id = 0) {

  //drupal_set_message($tax_id);
  if ($tax_id != 0) {
    $rule = db_fetch_object(db_query("SELECT * FROM {uc_taxes} WHERE id = %d", $tax_id));
    $form = array(
      'id' => array(
        '#type' => 'value',
        '#value' => $tax_id,
      ),
    );
    $output = confirm_form($form, t('Delete !rule?', array(
      '!rule' => $rule->name,
    )), 'admin/store/settings/taxes', '', t('Continue'), t('Cancel'));
    return $output;
  }
  else {
    drupal_goto('admin/store/settings/taxes');
  }
}

/**
 * Submit handler for uc_taxes_delete().
 */
function uc_taxes_delete_submit($form_id, $form_values) {
  if ($form_values['confirm']) {
    db_query("DELETE FROM {uc_taxes} WHERE id = %d", $form_values['id']);
    db_query("DELETE FROM {workflow_ng_cfgs} WHERE name = '%s'", 'uc_taxes_' . $form_values['id']);
    workflow_ng_clear_cache();
  }
  drupal_set_message(t('Tax rule deleted.'));
  return 'admin/store/settings/taxes';
}

/**
 * Handle the tax line item.
 */
function uc_line_item_tax($op, $order) {
  switch ($op) {
    case 'cart-preview':

      /* $items = $order;
         $order = new stdClass();
         $order->products = $items; */
      $taxes = module_invoke_all('calculate_tax', $order);
      $script = '';
      foreach ($taxes as $tax) {
        $script .= "set_line_item('tax_" . $tax['id'] . "', '" . $tax['name'] . "', " . $tax['amount'] . ", + " . variable_get('uc_li_tax_weight', 9) + $tax['weight'] / 10 . ", 1, false);\n";
      }
      if ($script) {
        uc_add_js("\$(document).ready(function() {\n          if (window.set_line_item) {\n            " . $script . ";\n            render_line_items();\n          }\n        });", 'inline');
      }
      break;
    case 'load':
      $lines = array();
      $taxes = module_invoke_all('calculate_tax', $order);
      foreach ($taxes as $tax) {
        $lines[] = array(
          'id' => 'tax',
          'title' => $tax['name'],
          'amount' => $tax['amount'],
          'weight' => variable_get('uc_li_tax_weight', 9) + $tax['weight'] / 10,
          'data' => $tax['data'],
        );
      }
      return $lines;
  }
}
function uc_line_item_tax_subtotal($op, $order) {
  $amount = 0;
  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 (substr($line_item['type'], 0, 3) != 'tax') {
        $amount += $line_item['amount'];
        $different = true;
      }
      else {
        $has_taxes = true;
      }
    }
  }
  if (is_array($order->taxes)) {
    $has_taxes = true;
  }
  if ($different && $has_taxes) {
    switch ($op) {
      case 'cart-preview':
        uc_add_js("\$(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
 ******************************************************************************/

/**
 * Load all tax rates.
 */
function uc_taxes_get_rates() {
  static $taxes = array();
  if (empty($taxes)) {
    $result = db_query("SELECT * FROM {uc_taxes} ORDER BY weight");
    while ($rule = db_fetch_object($result)) {
      $rule->taxed_product_types = unserialize($rule->taxed_product_types);
      $rule->taxed_line_items = unserialize($rule->taxed_line_items);
      $taxes[$rule->id] = $rule;
    }
  }
  return $taxes;
}

/**
 * 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;
}

/**
 * Calculate 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,
    ));
  }
  else {
    if ((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;
  }
  if (is_array($order->line_items)) {
    foreach ($order->line_items as $i => $line) {
      if (substr($line['type'], 0, 4) == 'tax_' && substr($line['type'], 5) != 'subtotal') {
        unset($order->line_items[$i]);
      }
    }
  }
  $_SESSION['taxes'] = array();
  $taxes = uc_taxes_get_rates();
  foreach ($taxes as $tax) {

    // Gotta pass a fake line_item entity for the data to be saved to $_SESSION.
    workflow_ng_invoke_event('calculate_tax_' . $tax->id, $order, $tax, $account, array());

    //$order->line_items[] = array('type' => 'tax', 'amount' => $_SESSION['taxes'][$tax->id]['amount']);
  }
  $order->taxes = $_SESSION['taxes'];
  unset($_SESSION['taxes']);

  //array_unshift($order->taxes, array('id' => 'subtotal', 'name' => t('Subtotal excluding taxes'), 'amount' => $amount, 'weight' => -10));
  return $order->taxes;
}

/**
 * AJAX callback for order preview.
 *
 * Calculate tax amounts for an order in the payment checkout pane.
 */
function uc_taxes_javascript() {
  drupal_set_header("Content-Type: text/javascript; charset=utf-8");
  $order = $_POST['order'];
  if ($order = unserialize(rawurldecode($order))) {
    $taxes = module_invoke_all('calculate_tax', $order);
    $subtotal = uc_line_item_tax_subtotal('load', $order);
    if (is_array($subtotal) && !empty($taxes)) {
      $taxes['subtotal'] = array(
        'id' => 'subtotal',
        'name' => $subtotal[0]['title'],
        'amount' => $subtotal[0]['amount'],
        'weight' => -10,
      );
    }

    //drupal_set_message('<pre>'. print_r($taxes, true) .'</pre>');
    if (count($taxes)) {
      print drupal_to_js((array) $taxes);
    }
    else {
      print '{}';
    }
  }
  else {
    print '{}';
  }
  exit;
}

// Returns the path to the UI for Workflow-ng based on the installed version.
function _workflow_ng_ui_path($url = FALSE) {
  return $url ? url(WORKFLOW_NG_UI_PATH) : WORKFLOW_NG_UI_PATH;
}

Functions

Namesort descending Description
uc_line_item_tax Handle the tax line item.
uc_line_item_tax_subtotal
uc_taxes_admin_settings Display a list of tax rates.
uc_taxes_calculate Calculates the taxes for an order based on enabled tax modules.
uc_taxes_calculate_tax Calculate the amount and types of taxes that apply to an order.
uc_taxes_copy Copy a tax rule.
uc_taxes_delete Delete a tax rule.
uc_taxes_delete_submit Submit handler for uc_taxes_delete().
uc_taxes_form Create or edit a tax rule.
uc_taxes_form_alter
uc_taxes_form_submit Submit handler for uc_taxes_form().
uc_taxes_get_rates Load all tax rates.
uc_taxes_help
uc_taxes_javascript AJAX callback for order preview.
uc_taxes_line_item Implementation of Übercart's hook_line_item().
uc_taxes_menu Implementation of hook_menu().
uc_taxes_order Update and save tax line items to the order.
uc_taxes_perm Implementation of hook_perm().
_workflow_ng_ui_path