<?php
// $Id: uc_taxes.module,v 1.12.2.20 2010/01/27 22:23:32 islandusurper Exp $

/**
 * @file
 * Ubercart Taxes module.
 *
 * Allows tax rules to be set up and applied to orders.
 */

require_once('uc_taxes.ca.inc');

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

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

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

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

/**
 * Implementation of 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
 ******************************************************************************/

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

/**
 * Implementation of hook_order().
 *
 * 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);
      $context = array(
        'revision' => 'formatted',
        'type' => 'line_item',
        'subject' => array(
          'order' => $arg1,
        ),
      );
      if (is_array($arg1->line_items)) {
        //drupal_set_message('<pre>'. var_export($arg1->line_items, TRUE) .'</pre>');
        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                                                             *
 ******************************************************************************/

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

/**
 * Handle the line item subtotal before taxes.
 */
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 ($line_item['type'] == 'subtotal') {
        continue;
      }
      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':
        drupal_add_js("if (Drupal.jsEnabled) { \$(document).ready(function() {
          if (window.set_line_item) {
            set_line_item('tax_subtotal', '". t('Subtotal excluding taxes') ."', ". $amount .", ". variable_get('uc_li_tax_subtotal_weight', 8) .");
          }
        })};", '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
 ******************************************************************************/

/**
 * Save 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);
  }
  // Otherwise update the existing tax rate's data.
  else {
    drupal_write_record('uc_taxes', $rate, array('id'));
  }

  return $rate;
}

/**
 * Load 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];
  }
  // Or return the whole shebang.
  else {
    return $rates;
  }
}

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

/**
 * Calculate 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));
  }
  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'),
      // #data => each $tax in the following foreach() loop;
    ),
    'account' => array(
      '#entity' => 'user',
      '#title' => t('User'),
      '#data' => $account,
    ),
  );

  $predicates = ca_load_trigger_predicates('calculate_taxes');
  foreach (uc_taxes_rate_load() as $tax) {
    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.
 *
 * Calculate 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);
}

/**
 * Calculate 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)) {
    return $item->price * $item->qty;
  }
}

/**
 * Apply 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)) {
        $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;
  }
}
