FINAL suepr merge step : added all modules to this super repos
This commit is contained in:
@@ -0,0 +1,331 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Tax tests.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Tests the tax functionality.
|
||||
*/
|
||||
class UbercartTaxesTestCase extends UbercartTestHelper {
|
||||
|
||||
public static function getInfo() {
|
||||
return array(
|
||||
'name' => 'Taxes',
|
||||
'description' => 'Ensures that taxes are calculated, stored and displayed correctly.',
|
||||
'group' => 'Ubercart',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Overrides DrupalWebTestCase::setUp().
|
||||
*/
|
||||
function setUp() {
|
||||
$modules = array('uc_product_kit', 'uc_attribute', 'uc_cart', 'uc_payment', 'uc_payment_pack', 'uc_taxes');
|
||||
$permissions = array('bypass node access', 'administer content types', 'administer rules', 'configure taxes');
|
||||
parent::setUp($modules, $permissions);
|
||||
}
|
||||
|
||||
function testInclusiveTaxes() {
|
||||
$this->drupalLogin($this->adminUser);
|
||||
|
||||
// Create a 20% inclusive tax rate.
|
||||
$rate = (object) array(
|
||||
'name' => $this->randomName(8),
|
||||
'rate' => 0.2,
|
||||
'taxed_product_types' => array('product'),
|
||||
'taxed_line_items' => array(),
|
||||
'weight' => 0,
|
||||
'shippable' => 0,
|
||||
'display_include' => 1,
|
||||
'inclusion_text' => $this->randomName(6),
|
||||
);
|
||||
uc_taxes_rate_save($rate);
|
||||
|
||||
// Ensure Rules picks up the new condition.
|
||||
entity_flush_caches();
|
||||
|
||||
// Create a $10 product.
|
||||
$product = $this->createProduct(array(
|
||||
'sell_price' => 10,
|
||||
));
|
||||
|
||||
// Create an attribute.
|
||||
$attribute = (object) array(
|
||||
'name' => $this->randomName(8),
|
||||
'label' => $this->randomName(8),
|
||||
'description' => $this->randomName(8),
|
||||
'required' => TRUE,
|
||||
'display' => 1,
|
||||
'ordering' => 0,
|
||||
);
|
||||
uc_attribute_save($attribute);
|
||||
|
||||
// Create an option with a price adjustment of $5.
|
||||
$option = (object) array(
|
||||
'aid' => $attribute->aid,
|
||||
'name' => $this->randomName(8),
|
||||
'cost' => 0,
|
||||
'price' => 5,
|
||||
'weight' => 0,
|
||||
'ordering' => 0,
|
||||
);
|
||||
uc_attribute_option_save($option);
|
||||
|
||||
// Attach the attribute to the product.
|
||||
$attribute = uc_attribute_load($attribute->aid);
|
||||
uc_attribute_subject_save($attribute, 'product', $product->nid, TRUE);
|
||||
|
||||
// Create a product kit containing the product.
|
||||
$kit = $this->drupalCreateNode(array(
|
||||
'type' => 'product_kit',
|
||||
'products' => array($product->nid),
|
||||
'ordering' => 0,
|
||||
'mutable' => UC_PRODUCT_KIT_UNMUTABLE_WITH_LIST,
|
||||
'default_qty' => 1,
|
||||
));
|
||||
|
||||
// Set the kit total to $9 to automatically apply a discount.
|
||||
$kit = node_load($kit->nid);
|
||||
$kit->kit_total = 9;
|
||||
node_save($kit);
|
||||
$kit = node_load($kit->nid);
|
||||
$this->assertEqual($kit->products[$product->nid]->discount, -1, 'Product kit component has correct discount applied.');
|
||||
|
||||
// Ensure the price is displayed tax-inclusively on the add-to-cart form.
|
||||
$this->drupalGet('node/' . $kit->nid);
|
||||
$this->assertText('$10.80' . $rate->inclusion_text, 'Tax inclusive price on node-view form is accurate.'); // $10.80 = $9.00 + 20%
|
||||
$this->assertRaw($option->name . ', +$6.00</option>', 'Tax inclusive option price on node view form is accurate.'); // $6.00 = $5.00 + 20%
|
||||
|
||||
// Add the product kit to the cart, selecting the option.
|
||||
$attribute_key = 'products[' . $product->nid . '][attributes][' . $attribute->aid . ']';
|
||||
$this->drupalPost('node/' . $kit->nid, array($attribute_key => $option->oid), t('Add to cart'));
|
||||
|
||||
// Check that the subtotal is $16.80 ($10 base + $5 option - $1 discount, with 20% tax)
|
||||
$this->drupalGet('cart');
|
||||
$this->assertText('Subtotal: $16.80', 'Order subtotal is correct on cart page.');
|
||||
|
||||
// Make sure that the subtotal is also correct on the checkout page.
|
||||
$this->drupalPost('cart', array(), 'Checkout');
|
||||
$this->assertText('Subtotal: $16.80', 'Order subtotal is correct on checkout page.');
|
||||
|
||||
// Manually proceed to checkout review.
|
||||
$zone_id = db_query_range('SELECT zone_id FROM {uc_zones} WHERE zone_country_id = :country ORDER BY rand()', 0, 1, array('country' => variable_get('uc_store_country', 840)))->fetchField();
|
||||
$edit = array(
|
||||
'panes[delivery][delivery_first_name]' => $this->randomName(10),
|
||||
'panes[delivery][delivery_last_name]' => $this->randomName(10),
|
||||
'panes[delivery][delivery_street1]' => $this->randomName(10),
|
||||
'panes[delivery][delivery_city]' => $this->randomName(10),
|
||||
'panes[delivery][delivery_zone]' => $zone_id,
|
||||
'panes[delivery][delivery_postal_code]' => mt_rand(10000, 99999),
|
||||
|
||||
'panes[billing][billing_first_name]' => $this->randomName(10),
|
||||
'panes[billing][billing_last_name]' => $this->randomName(10),
|
||||
'panes[billing][billing_street1]' => $this->randomName(10),
|
||||
'panes[billing][billing_city]' => $this->randomName(10),
|
||||
'panes[billing][billing_zone]' => $zone_id,
|
||||
'panes[billing][billing_postal_code]' => mt_rand(10000, 99999),
|
||||
);
|
||||
$this->drupalPost('cart/checkout', $edit, t('Review order'));
|
||||
$this->assertRaw(t('Your order is almost complete.'));
|
||||
|
||||
// Make sure the price is still listed tax-inclusively.
|
||||
// !TODO This could be handled more specifically with a regex.
|
||||
$this->assertText('$16.80' . $rate->inclusion_text, 'Tax inclusive price appears in cart pane on checkout review page');
|
||||
|
||||
// Ensure the tax-inclusive price is listed on the order admin page.
|
||||
$order_id = db_query("SELECT order_id FROM {uc_orders} WHERE delivery_first_name = :name", array(':name' => $edit['panes[delivery][delivery_first_name]']))->fetchField();
|
||||
$this->assertTrue($order_id, 'Order was created successfully');
|
||||
$this->drupalGet('admin/store/orders/' . $order_id);
|
||||
$this->assertText('$16.80' . $rate->inclusion_text, 'Tax inclusive price appears on the order view page.');
|
||||
|
||||
// And on the invoice.
|
||||
$this->drupalGet('admin/store/orders/' . $order_id . '/invoice');
|
||||
$this->assertText('$16.80' . $rate->inclusion_text, 'Tax inclusive price appears on the invoice.');
|
||||
|
||||
// And on the printable invoice.
|
||||
$this->drupalGet('admin/store/orders/' . $order_id . '/invoice');
|
||||
$this->assertText('$16.80' . $rate->inclusion_text, 'Tax inclusive price appears on the printable invoice.');
|
||||
}
|
||||
|
||||
function loadTaxLine($order_id) {
|
||||
$order = uc_order_load($order_id, TRUE);
|
||||
foreach ($order->line_items as $line) {
|
||||
if ($line['type'] == 'tax') {
|
||||
return $line;
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
function assertTaxLineCorrect($line, $rate, $when) {
|
||||
$this->assertTrue($line, t('The tax line item was saved to the order ' . $when));
|
||||
$this->assertTrue(number_format($rate * $this->product->sell_price, 2) == number_format($line['amount'], 2), t('Stored tax line item has the correct amount ' . $when));
|
||||
$this->assertFieldByName('line_items[' . $line['line_item_id'] . '][li_id]', $line['line_item_id'], t('Found the tax line item ID ' . $when));
|
||||
$this->assertText($line['title'], t('Found the tax title ' . $when));
|
||||
$this->assertText(uc_currency_format($line['amount']), t('Tax display has the correct amount ' . $when));
|
||||
}
|
||||
|
||||
function testStoredTaxDisplay() {
|
||||
$this->drupalLogin($this->adminUser);
|
||||
|
||||
// Enable a payment method for the payment preview checkout pane.
|
||||
$edit = array('uc_payment_method_check_checkout' => 1);
|
||||
$this->drupalPost('admin/store/settings/payment', $edit, t('Save configuration'));
|
||||
|
||||
// Create a 20% inclusive tax rate.
|
||||
$rate = (object) array(
|
||||
'name' => $this->randomName(8),
|
||||
'rate' => 0.2,
|
||||
'taxed_product_types' => array('product'),
|
||||
'taxed_line_items' => array(),
|
||||
'weight' => 0,
|
||||
'shippable' => 0,
|
||||
'display_include' => 1,
|
||||
'inclusion_text' => '',
|
||||
);
|
||||
uc_taxes_rate_save($rate);
|
||||
|
||||
$this->drupalGet('admin/store/settings/taxes');
|
||||
$this->assertText($rate->name, t('Tax was saved successfully.'));
|
||||
|
||||
$this->drupalGet("admin/store/settings/taxes/manage/uc_taxes_$rate->id");
|
||||
$this->assertText(t('Conditions'), t('Rules configuration linked to tax.'));
|
||||
|
||||
$this->drupalPost('node/' . $this->product->nid, array(), t('Add to cart'));
|
||||
|
||||
// Manually step through checkout. $this->checkout() doesn't know about taxes.
|
||||
$this->drupalPost('cart', array(), 'Checkout');
|
||||
$this->assertText(
|
||||
t('Enter your billing address and information here.'),
|
||||
t('Viewed cart page: Billing pane has been displayed.')
|
||||
);
|
||||
$this->assertRaw($rate->name, t('Tax line item displayed.'));
|
||||
$this->assertRaw(uc_currency_format($rate->rate * $this->product->sell_price), t('Correct tax amount displayed.'));
|
||||
|
||||
// Build the panes.
|
||||
$zone_id = db_query_range('SELECT zone_id FROM {uc_zones} WHERE zone_country_id = :country ORDER BY rand()', 0, 1, array('country' => variable_get('uc_store_country', 840)))->fetchField();
|
||||
$edit = array(
|
||||
'panes[delivery][delivery_first_name]' => $this->randomName(10),
|
||||
'panes[delivery][delivery_last_name]' => $this->randomName(10),
|
||||
'panes[delivery][delivery_street1]' => $this->randomName(10),
|
||||
'panes[delivery][delivery_city]' => $this->randomName(10),
|
||||
'panes[delivery][delivery_zone]' => $zone_id,
|
||||
'panes[delivery][delivery_postal_code]' => mt_rand(10000, 99999),
|
||||
|
||||
'panes[billing][billing_first_name]' => $this->randomName(10),
|
||||
'panes[billing][billing_last_name]' => $this->randomName(10),
|
||||
'panes[billing][billing_street1]' => $this->randomName(10),
|
||||
'panes[billing][billing_city]' => $this->randomName(10),
|
||||
'panes[billing][billing_zone]' => $zone_id,
|
||||
'panes[billing][billing_postal_code]' => mt_rand(10000, 99999),
|
||||
);
|
||||
|
||||
// Submit the checkout page.
|
||||
$this->drupalPost('cart/checkout', $edit, t('Review order'));
|
||||
$this->assertRaw(t('Your order is almost complete.'));
|
||||
$this->assertRaw($rate->name, t('Tax line item displayed.'));
|
||||
$this->assertRaw(uc_currency_format($rate->rate * $this->product->sell_price), t('Correct tax amount displayed.'));
|
||||
|
||||
// Complete the review page.
|
||||
$this->drupalPost(NULL, array(), t('Submit order'));
|
||||
|
||||
$order_id = db_query("SELECT order_id FROM {uc_orders} WHERE delivery_first_name = :name", array(':name' => $edit['panes[delivery][delivery_first_name]']))->fetchField();
|
||||
if ($order_id) {
|
||||
$this->pass(
|
||||
t('Order %order_id has been created', array('%order_id' => $order_id))
|
||||
);
|
||||
|
||||
$this->drupalGet('admin/store/orders/' . $order_id . '/edit');
|
||||
$this->assertTaxLineCorrect($this->loadTaxLine($order_id), $rate->rate, 'on initial order load');
|
||||
|
||||
$this->drupalPost('admin/store/orders/' . $order_id . '/edit', array(), t('Submit changes'));
|
||||
$this->assertText(t('Order changes saved.'));
|
||||
$this->assertTaxLineCorrect($this->loadTaxLine($order_id), $rate->rate, 'after saving order');
|
||||
|
||||
// Change tax rate and ensure order doesn't change.
|
||||
$oldrate = $rate->rate;
|
||||
$rate->rate = 0.1;
|
||||
$rate = uc_taxes_rate_save($rate);
|
||||
|
||||
// Save order because tax changes are only updated on save.
|
||||
$this->drupalPost('admin/store/orders/' . $order_id . '/edit', array(), t('Submit changes'));
|
||||
$this->assertText(t('Order changes saved.'));
|
||||
$this->assertTaxLineCorrect($this->loadTaxLine($order_id), $oldrate, 'after rate change');
|
||||
|
||||
// Change taxable products and ensure order doesn't change.
|
||||
$class = $this->createProductClass();
|
||||
$rate->taxed_product_types = array($class->name);
|
||||
uc_taxes_rate_save($rate);
|
||||
entity_flush_caches();
|
||||
$this->drupalPost('admin/store/orders/' . $order_id . '/edit', array(), t('Submit changes'));
|
||||
$this->assertText(t('Order changes saved.'));
|
||||
$this->assertTaxLineCorrect($this->loadTaxLine($order_id), $oldrate, 'after applicable product change');
|
||||
|
||||
// Change order Status back to in_checkout and ensure tax-rate changes now update the order.
|
||||
uc_order_update_status($order_id, 'in_checkout');
|
||||
$this->drupalPost('admin/store/orders/' . $order_id . '/edit', array(), t('Submit changes'));
|
||||
$this->assertText(t('Order changes saved.'));
|
||||
$this->assertFalse($this->loadTaxLine($order_id), t('The tax line was removed from the order when order status changed back to in_checkout.'));
|
||||
|
||||
// Restore taxable product and ensure new tax is added.
|
||||
$rate->taxed_product_types = array('product');
|
||||
uc_taxes_rate_save($rate);
|
||||
$this->drupalPost('admin/store/orders/' . $order_id . '/edit', array(), t('Submit changes'));
|
||||
$this->assertText(t('Order changes saved.'));
|
||||
$this->assertTaxLineCorrect($this->loadTaxLine($order_id), $rate->rate, 'when order status changed back to in_checkout');
|
||||
}
|
||||
else {
|
||||
$this->fail(t('No order was created.'));
|
||||
}
|
||||
}
|
||||
|
||||
function testTaxProductClassUpdate() {
|
||||
$this->drupalLogin($this->adminUser);
|
||||
|
||||
// Create a new product class.
|
||||
$type = strtolower($this->randomName(12));
|
||||
$edit = array(
|
||||
'pcid' => $type,
|
||||
'name' => $type,
|
||||
'description' => $this->randomName(32),
|
||||
);
|
||||
$this->drupalPost('admin/store/products/classes', $edit, t('Save'));
|
||||
node_types_rebuild();
|
||||
|
||||
// Create a tax rate.
|
||||
$tax = $this->randomName(8);
|
||||
$rate = (object) array(
|
||||
'id' => 0, // TODO: should not have to set this
|
||||
'name' => $tax,
|
||||
'rate' => rand(1, 20) / 10,
|
||||
'taxed_product_types' => array($type),
|
||||
'taxed_line_items' => array(),
|
||||
'weight' => 0,
|
||||
'shippable' => 0,
|
||||
);
|
||||
uc_taxes_rate_save($rate);
|
||||
|
||||
// Check that the tax rate shows up at checkout.
|
||||
$product = $this->createProduct(array('type' => $type));
|
||||
$this->drupalPost('node/' . $product->nid, array(), t('Add to cart'));
|
||||
$this->drupalGet('cart/checkout');
|
||||
$this->assertText($tax, 'Tax line item displayed.');
|
||||
|
||||
// Change the machine name of the product class.
|
||||
$new_type = strtolower($this->randomName(12));
|
||||
$edit = array(
|
||||
'name' => $new_type,
|
||||
'type' => $new_type,
|
||||
);
|
||||
$this->drupalPost('admin/structure/types/manage/' . $type, $edit, t('Save content type'));
|
||||
|
||||
// Check that the tax rate still shows up at checkout.
|
||||
$this->drupalPost('cart', array(), t('Remove'));
|
||||
$this->drupalPost('node/' . $product->nid, array(), t('Add to cart'));
|
||||
$this->drupalGet('cart/checkout');
|
||||
$this->assertText($tax, 'Tax line item displayed after changing product class node type.');
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,278 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Taxes administration menu items.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Displays a list of tax rates.
|
||||
*/
|
||||
function uc_taxes_admin_settings() {
|
||||
$header = array(t('Name'), t('Rate'), t('Taxed products'), t('Taxed product types'), t('Taxed line items'), t('Weight'), array('data' => t('Operations'), 'colspan' => 4));
|
||||
|
||||
$rows = array();
|
||||
foreach (uc_taxes_rate_load() as $rate_id => $rate) {
|
||||
$rows[] = array(
|
||||
check_plain($rate->name),
|
||||
$rate->rate * 100 . '%',
|
||||
$rate->shippable ? t('Shippable products') : t('Any product'),
|
||||
implode(', ', $rate->taxed_product_types),
|
||||
implode(', ', $rate->taxed_line_items),
|
||||
$rate->weight,
|
||||
l(t('edit'), 'admin/store/settings/taxes/' . $rate_id . '/edit'),
|
||||
l(t('conditions'), 'admin/store/settings/taxes/manage/uc_taxes_' . $rate_id),
|
||||
l(t('clone'), 'admin/store/settings/taxes/' . $rate_id . '/clone'),
|
||||
l(t('delete'), 'admin/store/settings/taxes/' . $rate_id . '/delete'),
|
||||
);
|
||||
}
|
||||
|
||||
$build['taxes'] = array(
|
||||
'#theme' => 'table',
|
||||
'#header' => $header,
|
||||
'#rows' => $rows,
|
||||
'#empty' => t('No rates available.'),
|
||||
);
|
||||
|
||||
return $build;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a form to add or edit a tax rate.
|
||||
*
|
||||
* @param $rate_id
|
||||
* The ID of the tax rate to edit; leave NULL to add a new rate.
|
||||
*
|
||||
* @see uc_taxes_form_validate()
|
||||
* @see uc_taxes_form_submit()
|
||||
* @ingroup forms
|
||||
*/
|
||||
function uc_taxes_form($form, &$form_state, $rate_id = NULL) {
|
||||
// If a rate ID was specified...
|
||||
if ($rate_id) {
|
||||
// Load the tax rate and set the page title.
|
||||
$rate = uc_taxes_rate_load($rate_id);
|
||||
|
||||
drupal_set_title($rate->name);
|
||||
}
|
||||
|
||||
$form['id'] = array(
|
||||
'#type' => 'value',
|
||||
'#value' => $rate_id,
|
||||
);
|
||||
|
||||
$form['name'] = array(
|
||||
'#type' => 'textfield',
|
||||
'#title' => t('Name'),
|
||||
'#description' => t('This name will appear to the customer when this tax is applied to an order.'),
|
||||
'#default_value' => $rate_id ? $rate->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' => $rate_id ? (($rate->rate * 100) . '%') : '',
|
||||
'#size' => 15,
|
||||
'#required' => TRUE,
|
||||
);
|
||||
|
||||
$form['shippable'] = array(
|
||||
'#type' => 'radios',
|
||||
'#title' => t('Taxed products'),
|
||||
'#options' => array(
|
||||
t('Apply tax to any product regardless of its shippability.'),
|
||||
t('Apply tax to shippable products only.'),
|
||||
),
|
||||
'#default_value' => $rate_id ? $rate->shippable : 0,
|
||||
);
|
||||
|
||||
// TODO: Remove the need for a special case for product kit module.
|
||||
$options = uc_product_type_names();
|
||||
unset($options['product_kit']);
|
||||
$options['blank-line'] = t('"Blank line" product');
|
||||
|
||||
$form['taxed_product_types'] = array(
|
||||
'#type' => 'checkboxes',
|
||||
'#title' => t('Taxed product types'),
|
||||
'#description' => t('Apply taxes to the specified product types/classes.'),
|
||||
'#options' => $options,
|
||||
'#default_value' => $rate_id ? $rate->taxed_product_types : array(),
|
||||
);
|
||||
|
||||
$options = array();
|
||||
|
||||
foreach (_uc_line_item_list() as $id => $line_item) {
|
||||
if (!in_array($id, array('subtotal', 'tax_subtotal', 'total', 'tax_display'))) {
|
||||
$options[$id] = $line_item['title'];
|
||||
}
|
||||
}
|
||||
|
||||
$form['taxed_line_items'] = array(
|
||||
'#type' => 'checkboxes',
|
||||
'#title' => t('Taxed line items'),
|
||||
'#description' => t('Adds the checked line item types to the total before applying this tax.'),
|
||||
'#options' => $options,
|
||||
'#default_value' => $rate_id ? $rate->taxed_line_items : array(),
|
||||
);
|
||||
|
||||
$form['weight'] = array(
|
||||
'#type' => 'weight',
|
||||
'#title' => t('Weight'),
|
||||
'#description' => t('Taxes are sorted by weight and then applied to the order sequentially. This value is important when taxes need to include other tax line items.'),
|
||||
'#default_value' => $rate_id ? $rate->weight : 0,
|
||||
);
|
||||
|
||||
$form['display_include'] = array(
|
||||
'#type' => 'checkbox',
|
||||
'#title' => t('Include this tax when displaying product prices.'),
|
||||
'#default_value' => $rate_id ? $rate->display_include : 0,
|
||||
);
|
||||
|
||||
$form['inclusion_text'] = array(
|
||||
'#type' => 'textfield',
|
||||
'#title' => t('Tax inclusion text'),
|
||||
'#description' => t('This text will be displayed near the price to indicate that it includes tax.'),
|
||||
'#default_value' => $rate_id ? $rate->inclusion_text : '',
|
||||
);
|
||||
|
||||
$form['actions'] = array('#type' => 'actions');
|
||||
$form['actions']['submit'] = array(
|
||||
'#type' => 'submit',
|
||||
'#value' => t('Submit'),
|
||||
'#suffix' => l(t('Cancel'), 'admin/store/settings/taxes'),
|
||||
);
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that tax rates are positive numbers.
|
||||
*
|
||||
* @see uc_taxes_form()
|
||||
* @see uc_taxes_form_submit()
|
||||
*/
|
||||
function uc_taxes_form_validate($form, &$form_state) {
|
||||
if (!empty($form_state['values']['rate']) && (floatval($form_state['values']['rate']) < 0)) {
|
||||
form_set_error('rate', t('Rate must be a positive number. No commas and only one decimal point.'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Form submission handler for uc_taxes_form().
|
||||
*
|
||||
* @see uc_taxes_form()
|
||||
* @see uc_taxes_form_validate()
|
||||
*/
|
||||
function uc_taxes_form_submit($form, &$form_state) {
|
||||
// Determine the decimal rate value.
|
||||
if (strpos($form_state['values']['rate'], '%')) {
|
||||
$form_state['values']['rate'] = floatval($form_state['values']['rate']) / 100;
|
||||
}
|
||||
else {
|
||||
$form_state['values']['rate'] = floatval($form_state['values']['rate']);
|
||||
}
|
||||
|
||||
// Build the rate object based on the form values and save it.
|
||||
$rate = (object) array(
|
||||
'id' => $form_state['values']['id'],
|
||||
'name' => $form_state['values']['name'],
|
||||
'rate' => $form_state['values']['rate'],
|
||||
'taxed_product_types' => array_filter($form_state['values']['taxed_product_types']),
|
||||
'taxed_line_items' => array_filter($form_state['values']['taxed_line_items']),
|
||||
'weight' => $form_state['values']['weight'],
|
||||
'shippable' => $form_state['values']['shippable'],
|
||||
'display_include' => $form_state['values']['display_include'],
|
||||
'inclusion_text' => $form_state['values']['inclusion_text'],
|
||||
);
|
||||
$rate = uc_taxes_rate_save($rate);
|
||||
|
||||
// Update the name of the associated conditions.
|
||||
$conditions = rules_config_load('uc_taxes_' . $form_state['values']['id']);
|
||||
if ($conditions) {
|
||||
$conditions->label = $form_state['values']['name'];
|
||||
$conditions->save();
|
||||
}
|
||||
|
||||
// Display a message and redirect back to the overview,
|
||||
// or the conditions page for new taxes.
|
||||
if ($form_state['values']['id']) {
|
||||
drupal_set_message(t('Tax rate %name saved.', array('%name' => $form_state['values']['name'])));
|
||||
$form_state['redirect'] = 'admin/store/settings/taxes';
|
||||
}
|
||||
else {
|
||||
drupal_set_message(t('Tax rate %name created.', array('%name' => $form_state['values']['name'])));
|
||||
$form_state['redirect'] = 'admin/store/settings/taxes/manage/uc_taxes_' . $rate->id;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clones a tax rate.
|
||||
*/
|
||||
function uc_taxes_clone($rate_id) {
|
||||
// Load the source rate object.
|
||||
$rate = uc_taxes_rate_load($rate_id);
|
||||
$name = $rate->name;
|
||||
|
||||
// Tweak the name and unset the rate ID.
|
||||
$rate->name = t('Copy of !name', array('!name' => $rate->name));
|
||||
$rate->id = NULL;
|
||||
|
||||
// Save the new rate without clearing the Rules cache.
|
||||
$rate = uc_taxes_rate_save($rate, FALSE);
|
||||
|
||||
// Clone the associated conditions as well.
|
||||
if ($conditions = rules_config_load('uc_taxes_' . $rate_id)) {
|
||||
$conditions->id = NULL;
|
||||
$conditions->name = '';
|
||||
$conditions->save('uc_taxes_' . $rate->id);
|
||||
}
|
||||
|
||||
entity_flush_caches();
|
||||
|
||||
// Display a message and redirect back to the overview.
|
||||
drupal_set_message(t('Tax rate %name cloned.', array('%name' => $name)));
|
||||
|
||||
drupal_goto('admin/store/settings/taxes');
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a tax rule.
|
||||
*
|
||||
* @see uc_taxes_delete_form_submit()
|
||||
* @ingroup forms
|
||||
*/
|
||||
function uc_taxes_delete_form($form, &$form_state, $rate_id) {
|
||||
// Bail if we got a bad rate ID.
|
||||
if (!$rate = uc_taxes_rate_load($rate_id)) {
|
||||
drupal_set_message(t('That tax rate does not exist.'), 'error');
|
||||
drupal_goto('admin/store/settings/taxes');
|
||||
}
|
||||
|
||||
$form['rate_id'] = array(
|
||||
'#type' => 'value',
|
||||
'#value' => $rate_id,
|
||||
);
|
||||
$form['name'] = array(
|
||||
'#type' => 'value',
|
||||
'#value' => $rate->name,
|
||||
);
|
||||
|
||||
return confirm_form($form, t('Are you sure you want to delete @name?', array('@name' => $rate->name)), 'admin/store/settings/taxes', t('This action cannot be undone. Any orders that have been charged this tax may lose tax if you proceed.<br />If you just want this tax to no longer be applied to orders, consider disabling its predicate instead.'), t('Delete'), t('Cancel'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Form submission handler for uc_taxes_delete_form().
|
||||
*
|
||||
* @see uc_taxes_delete_form()
|
||||
*/
|
||||
function uc_taxes_delete_form_submit($form, &$form_state) {
|
||||
// Delete the tax rate.
|
||||
uc_taxes_rate_delete($form_state['values']['rate_id']);
|
||||
|
||||
// Display a message and redirect back to the overview.
|
||||
drupal_set_message(t('Tax rate %name deleted.', array('%name' => $form_state['values']['name'])));
|
||||
|
||||
$form_state['redirect'] = 'admin/store/settings/taxes';
|
||||
}
|
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Hooks provided by the Taxes module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @addtogroup hooks
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Calculates tax line items for an order.
|
||||
*
|
||||
* @param $order
|
||||
* An order object or an order id.
|
||||
*
|
||||
* @return
|
||||
* An array of tax line item objects keyed by a module-specific id.
|
||||
*/
|
||||
function hook_uc_calculate_tax($order) {
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$set = rules_config_load('uc_taxes_' . $tax->id);
|
||||
if ($set->execute($order)) {
|
||||
$line_item = uc_taxes_apply_tax($order, $tax);
|
||||
if ($line_item) {
|
||||
$order->taxes[$line_item->id] = $line_item;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $order->taxes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @} End of "addtogroup hooks".
|
||||
*/
|
@@ -0,0 +1,19 @@
|
||||
name = Taxes
|
||||
description = Defines tax rates for customers' geographic locations and products sold.
|
||||
dependencies[] = uc_store
|
||||
dependencies[] = uc_payment
|
||||
dependencies[] = uc_product
|
||||
package = Ubercart - core (optional)
|
||||
core = 7.x
|
||||
|
||||
; Test cases
|
||||
files[] = tests/uc_taxes.test
|
||||
|
||||
configure = admin/store/settings/taxes
|
||||
|
||||
; Information added by Drupal.org packaging script on 2013-12-17
|
||||
version = "7.x-3.6"
|
||||
core = "7.x"
|
||||
project = "ubercart"
|
||||
datestamp = "1387304010"
|
||||
|
@@ -0,0 +1,253 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Install, update and uninstall functions for the uc_taxes module.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_schema().
|
||||
*/
|
||||
function uc_taxes_schema() {
|
||||
$schema = array();
|
||||
|
||||
$schema['uc_taxes'] = array(
|
||||
'description' => 'Stores tax information.',
|
||||
'fields' => array(
|
||||
'id' => array(
|
||||
'description' => 'Primary key: Unique tax rate id.',
|
||||
'type' => 'serial',
|
||||
'unsigned' => TRUE,
|
||||
'not null' => TRUE,
|
||||
),
|
||||
'name' => array(
|
||||
'description' => 'The tax rate name.',
|
||||
'type' => 'varchar',
|
||||
'length' => 255,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
),
|
||||
'rate' => array(
|
||||
'description' => 'The tax rate multiplier.',
|
||||
'type' => 'float',
|
||||
'not null' => TRUE,
|
||||
'default' => 0.0,
|
||||
),
|
||||
'shippable' => array(
|
||||
'description' => 'Flag that describes how this rate applies to shippable and non-shippable products. 0 => Disregard shipability. 1 => Apply tax to shippable products only.',
|
||||
'type' => 'int',
|
||||
'size' => 'tiny',
|
||||
'unsigned' => TRUE,
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
),
|
||||
'taxed_product_types' => array(
|
||||
'description' => 'Serialized array of node types to be taxed.',
|
||||
'type' => 'text',
|
||||
'serialize' => TRUE,
|
||||
),
|
||||
'taxed_line_items' => array(
|
||||
'description' => 'Serialized array of line item types to be taxed.',
|
||||
'type' => 'text',
|
||||
'serialize' => TRUE,
|
||||
),
|
||||
'weight' => array(
|
||||
'description' => 'The weight of this tax rate in relation to other rates.',
|
||||
'type' => 'int',
|
||||
'size' => 'tiny',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
),
|
||||
'display_include' => array(
|
||||
'description' => 'Boolean flag indicating that product prices should be displayed including the tax.',
|
||||
'type' => 'int',
|
||||
'size' => 'tiny',
|
||||
'unsigned' => TRUE,
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
),
|
||||
'inclusion_text' => array(
|
||||
'description' => 'Text to be shown near a product price that includes tax.',
|
||||
'type' => 'varchar',
|
||||
'length' => 255,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
),
|
||||
),
|
||||
'primary key' => array('id'),
|
||||
);
|
||||
|
||||
$schema['uc_taxed_product_types'] = array(
|
||||
'fields' => array(
|
||||
'tax_id' => array(
|
||||
'description' => 'Tax rate id',
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
),
|
||||
'type' => array(
|
||||
'description' => 'Node type',
|
||||
'type' => 'varchar',
|
||||
'length' => 32,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
),
|
||||
),
|
||||
'primary key' => array('tax_id', 'type'),
|
||||
'indexes' => array(
|
||||
'type' => array('type'),
|
||||
),
|
||||
);
|
||||
|
||||
$schema['uc_taxed_line_items'] = $schema['uc_taxed_product_types'];
|
||||
$schema['uc_taxed_line_items']['fields']['type']['description'] = 'Line item type';
|
||||
|
||||
return $schema;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_update_last_removed().
|
||||
*/
|
||||
function uc_taxes_update_last_removed() {
|
||||
return 6003;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add "price including tax" columns.
|
||||
*/
|
||||
function uc_taxes_update_7000() {
|
||||
db_add_field('uc_taxes', 'include', array(
|
||||
'description' => 'Boolean flag indicating that product prices should be displayed including the tax.',
|
||||
'type' => 'int',
|
||||
'size' => 'tiny',
|
||||
'unsigned' => TRUE,
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
));
|
||||
db_add_field('uc_taxes', 'inclusion_text', array(
|
||||
'description' => 'Text to be shown near a product price that includes tax.',
|
||||
'type' => 'varchar',
|
||||
'length' => 255,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
));
|
||||
|
||||
return t('Added "price including tax" columns.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Separate taxed product types and line items to joined tables.
|
||||
*/
|
||||
function uc_taxes_update_7001() {
|
||||
$table = array(
|
||||
'fields' => array(
|
||||
'tax_id' => array(
|
||||
'description' => 'Tax rate id',
|
||||
'type' => 'int',
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
),
|
||||
'type' => array(
|
||||
'description' => 'Node type',
|
||||
'type' => 'varchar',
|
||||
'length' => 32,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
),
|
||||
),
|
||||
'primary key' => array('tax_id', 'type'),
|
||||
'indexes' => array(
|
||||
'type' => array('type'),
|
||||
),
|
||||
);
|
||||
db_create_table('uc_taxed_product_types', $table);
|
||||
|
||||
$table['fields']['type']['description'] = 'Line item type';
|
||||
db_create_table('uc_taxed_line_items', $table);
|
||||
|
||||
$p_insert = db_insert('uc_taxed_product_types')->fields(array('tax_id', 'type'));
|
||||
$l_insert = db_insert('uc_taxed_line_items')->fields(array('tax_id', 'type'));
|
||||
|
||||
$result = db_query("SELECT id, taxed_product_types, taxed_line_items FROM {uc_taxes}");
|
||||
foreach ($result as $tax) {
|
||||
$tax->taxed_product_types = unserialize($tax->taxed_product_types);
|
||||
$tax->taxed_line_items = unserialize($tax->taxed_line_items);
|
||||
|
||||
foreach ($tax->taxed_product_types as $type) {
|
||||
$p_insert->values(array(
|
||||
'tax_id' => $tax->id,
|
||||
'type' => $type,
|
||||
));
|
||||
}
|
||||
|
||||
foreach ($tax->taxed_line_items as $type) {
|
||||
$l_insert->values(array(
|
||||
'tax_id' => $tax->id,
|
||||
'type' => $type,
|
||||
));
|
||||
}
|
||||
|
||||
$p_insert->execute();
|
||||
$l_insert->execute();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix schema mismatch in update 7000.
|
||||
*/
|
||||
function uc_taxes_update_7002() {
|
||||
if (db_field_exists('uc_taxes', 'include')) {
|
||||
db_change_field('uc_taxes', 'include', 'display_include', array(
|
||||
'description' => 'Boolean flag indicating that product prices should be displayed including the tax.',
|
||||
'type' => 'int',
|
||||
'size' => 'tiny',
|
||||
'unsigned' => TRUE,
|
||||
'not null' => TRUE,
|
||||
'default' => 0,
|
||||
));
|
||||
}
|
||||
|
||||
db_change_field('uc_taxes', 'inclusion_text', 'inclusion_text', array(
|
||||
'description' => 'Text to be shown near a product price that includes tax.',
|
||||
'type' => 'varchar',
|
||||
'length' => 255,
|
||||
'not null' => TRUE,
|
||||
'default' => '',
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Make sure orders have stored tax data.
|
||||
*/
|
||||
function uc_taxes_update_7003(&$sandbox) {
|
||||
if (!isset($sandbox['progress'])) {
|
||||
$taxes = db_query("SELECT id FROM {uc_taxes}")->fetchField();
|
||||
if (!$taxes) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sandbox['current'] = 0;
|
||||
$sandbox['progress'] = 0;
|
||||
$sandbox['max'] = db_query("SELECT COUNT(order_id) FROM {uc_orders}")->fetchField();
|
||||
}
|
||||
|
||||
$t = get_t();
|
||||
$limit = 100;
|
||||
|
||||
$orders = db_query_range("SELECT order_id FROM {uc_orders} WHERE order_id > :current ORDER BY order_id", 0, $limit, array(':current' => $sandbox['current']))->fetchCol();
|
||||
foreach ($orders as $order_id) {
|
||||
$order = uc_order_load($order_id);
|
||||
|
||||
uc_order_save($order);
|
||||
|
||||
$sandbox['current'] = $order_id;
|
||||
$sandbox['progress']++;
|
||||
}
|
||||
|
||||
if ($sandbox['progress'] < $sandbox['max']) {
|
||||
$sandbox['#finished'] = min(0.99, $sandbox['progress'] / $sandbox['max']);
|
||||
}
|
||||
else {
|
||||
$sandbox['#finished'] = 1;
|
||||
}
|
||||
}
|
@@ -0,0 +1,684 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Ubercart Taxes module.
|
||||
*
|
||||
* Allows tax rules to be set up and applied to orders.
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* Implements hook_permission().
|
||||
*/
|
||||
function uc_taxes_permission() {
|
||||
return array(
|
||||
'configure taxes' => array(
|
||||
'title' => t('Configure taxes'),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_menu().
|
||||
*/
|
||||
function uc_taxes_menu() {
|
||||
$items = array();
|
||||
|
||||
$items['admin/store/settings/taxes'] = array(
|
||||
'title' => 'Taxes',
|
||||
'description' => 'Configure tax rates and rules.',
|
||||
'page callback' => 'uc_taxes_admin_settings',
|
||||
'access arguments' => array('configure taxes'),
|
||||
'file' => 'uc_taxes.admin.inc',
|
||||
);
|
||||
$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',
|
||||
'type' => MENU_LOCAL_ACTION,
|
||||
);
|
||||
$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'),
|
||||
'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'),
|
||||
'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'),
|
||||
'file' => 'uc_taxes.admin.inc',
|
||||
);
|
||||
|
||||
$items += rules_ui()->config_menu('admin/store/settings/taxes');
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_module_implements_alter().
|
||||
*
|
||||
* Ensures that all other line items are added to the order before tax
|
||||
* calculations are made.
|
||||
*/
|
||||
function uc_taxes_module_implements_alter(&$implementations, $hook) {
|
||||
if (in_array($hook, array('uc_order', 'entity_view_alter'))) {
|
||||
$group = $implementations['uc_taxes'];
|
||||
unset($implementations['uc_taxes']);
|
||||
$implementations['uc_taxes'] = $group;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_form_uc_order_edit_form_alter().
|
||||
*/
|
||||
function uc_taxes_form_uc_order_edit_form_alter(&$form, &$form_state) {
|
||||
$order = $form['#order'];
|
||||
$line_items = $order->line_items;
|
||||
foreach ($line_items as $item) {
|
||||
// Tax line items are stored in the database, but they can't be changed by
|
||||
// the user.
|
||||
if ($item['type'] == 'tax') {
|
||||
$form['line_items'][$item['line_item_id']]['title'] = array(
|
||||
'#markup' => check_plain($item['title']),
|
||||
);
|
||||
$form['line_items'][$item['line_item_id']]['amount'] = array(
|
||||
'#theme' => 'uc_price',
|
||||
'#price' => $item['amount'],
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_entity_view_alter().
|
||||
*
|
||||
* Adds included taxes (VAT) to display price of applicable products.
|
||||
*/
|
||||
function uc_taxes_entity_view_alter(&$build, $entity_type) {
|
||||
switch ($entity_type) {
|
||||
case 'node':
|
||||
if (uc_product_is_product($build['#node'])) {
|
||||
list($amount, $suffixes) = uc_taxes_get_included_tax($build['#node']);
|
||||
$build['display_price']['#value'] += $amount;
|
||||
if (!empty($suffixes)) {
|
||||
$build['display_price']['#suffixes'] += $suffixes;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case 'uc_cart_item':
|
||||
list($amount, $suffixes) = uc_taxes_get_included_tax($build['#entity'], isset($build['#entity']->order) ? $build['#entity']->order : NULL);
|
||||
|
||||
if (!empty($amount) && !empty($build['#total'])) {
|
||||
$build['#total'] += $amount * $build['qty']['#default_value'];
|
||||
}
|
||||
|
||||
if (!empty($suffixes)) {
|
||||
if (empty($build['#suffixes'])) {
|
||||
$build['#suffixes'] = array();
|
||||
}
|
||||
$build['#suffixes'] += $suffixes;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'uc_order_product':
|
||||
list($amount, $suffixes) = uc_taxes_get_included_tax($build['#entity'], isset($build['#entity']->order) ? $build['#entity']->order : NULL);
|
||||
|
||||
$build['price']['#price'] += $amount;
|
||||
$build['total']['#price'] += $amount * $build['#entity']->qty;
|
||||
$build['price']['#suffixes'] += $suffixes;
|
||||
$build['total']['#suffixes'] += $suffixes;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_uc_line_item().
|
||||
*/
|
||||
function uc_taxes_uc_line_item() {
|
||||
$items['tax'] = array(
|
||||
'title' => t('Tax'),
|
||||
'weight' => 9,
|
||||
'stored' => TRUE,
|
||||
'default' => FALSE,
|
||||
'calculated' => TRUE,
|
||||
'display_only' => FALSE,
|
||||
);
|
||||
$items['tax_display'] = array(
|
||||
'title' => t('Tax'),
|
||||
'callback' => 'uc_line_item_tax_display',
|
||||
'weight' => 5,
|
||||
'stored' => FALSE,
|
||||
'calculated' => TRUE,
|
||||
'display_only' => TRUE,
|
||||
);
|
||||
$items['tax_subtotal'] = array(
|
||||
'title' => t('Subtotal excluding taxes'),
|
||||
'callback' => 'uc_line_item_tax_subtotal',
|
||||
'weight' => 7,
|
||||
'stored' => FALSE,
|
||||
'calculated' => FALSE,
|
||||
'display_only' => TRUE,
|
||||
);
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_uc_order().
|
||||
*
|
||||
* Updates and saves tax line items to the order.
|
||||
*/
|
||||
function uc_taxes_uc_order($op, $order, $arg2) {
|
||||
switch ($op) {
|
||||
case 'save':
|
||||
$changes = array();
|
||||
$line_items = uc_taxes_calculate($order);
|
||||
foreach ($line_items as $id => $tax) {
|
||||
$line_items[$id] = _uc_taxes_to_line_item($tax);
|
||||
}
|
||||
|
||||
// Loop through existing line items and update or delete as necessary.
|
||||
if (is_array($order->line_items)) {
|
||||
foreach ($order->line_items as $i => $line) {
|
||||
if ($line['type'] == 'tax') {
|
||||
$delete = TRUE;
|
||||
foreach ($line_items as $id => $new_line) {
|
||||
if ($new_line['data']['tax_id'] == $line['data']['tax_id']) {
|
||||
if ($new_line['amount'] != $line['amount']) {
|
||||
uc_order_update_line_item($line['line_item_id'], $new_line['title'], $new_line['amount'], $new_line['data']);
|
||||
$order->line_items[$i]['amount'] = $new_line['amount'];
|
||||
$order->line_items[$i]['data'] = $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']);
|
||||
unset($order->line_items[$i]);
|
||||
$changes[] = t('Removed %title.', array('%title' => $line['title']));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now add line items for any remaining new taxes.
|
||||
if (is_array($line_items)) {
|
||||
foreach ($line_items as $line) {
|
||||
$order->line_items[] = uc_order_line_item_add($order->order_id, 'tax', $line['title'], $line['amount'], $line['weight'], $line['data']);
|
||||
$changes[] = t('Added %amount for %title.', array('%amount' => uc_currency_format($line['amount']), '%title' => $line['title']));
|
||||
}
|
||||
}
|
||||
|
||||
// And log the changes to the order.
|
||||
if (count($changes)) {
|
||||
uc_order_log_changes($order->order_id, $changes);
|
||||
usort($order->line_items, 'uc_weight_sort');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_node_type_update().
|
||||
*
|
||||
* Ensure taxed product type are synchronised if the content type is updated.
|
||||
*/
|
||||
function uc_taxes_node_type_update($info) {
|
||||
$existing_type = !empty($info->old_type) ? $info->old_type : $info->type;
|
||||
|
||||
db_update('uc_taxed_product_types')
|
||||
->fields(array(
|
||||
'type' => $info->type,
|
||||
))
|
||||
->condition('type', $existing_type)
|
||||
->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tax line item callback.
|
||||
*/
|
||||
function uc_line_item_tax_display($op, $order) {
|
||||
switch ($op) {
|
||||
case 'display':
|
||||
$lines = array();
|
||||
$taxes = uc_taxes_calculate($order);
|
||||
foreach ($taxes as $tax) {
|
||||
foreach ($order->line_items as $line_item) {
|
||||
if ($line_item['type'] == 'tax' && $line_item['data']['tax_id'] == $tax->id) {
|
||||
continue 2;
|
||||
}
|
||||
}
|
||||
$lines[] = _uc_taxes_to_line_item($tax);
|
||||
}
|
||||
return $lines;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a tax object to the format expected by line item callbacks.
|
||||
*
|
||||
* @param $tax
|
||||
* A tax object as returned by hook_uc_taxes_calculate().
|
||||
* @return
|
||||
* A line item array suitable for returning from line item callbacks.
|
||||
*/
|
||||
function _uc_taxes_to_line_item($tax) {
|
||||
$line = array(
|
||||
'id' => ($tax->summed ? 'tax' : 'tax_included'),
|
||||
'title' => !empty($tax->name) ? $tax->name : $tax->id,
|
||||
'amount' => $tax->amount,
|
||||
'weight' => variable_get('uc_li_tax_weight', 9) + (!empty($tax->weight) ? $tax->weight / 10 : 0),
|
||||
'data' => isset($tax->data) ? $tax->data : array(),
|
||||
);
|
||||
$line['data']['tax_id'] = $tax->id;
|
||||
return $line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the line item subtotal before taxes.
|
||||
*/
|
||||
function uc_line_item_tax_subtotal($op, $order) {
|
||||
$amount = 0;
|
||||
switch ($op) {
|
||||
case 'display':
|
||||
$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) && count($order->taxes)) {
|
||||
$has_taxes = TRUE;
|
||||
}
|
||||
|
||||
if ($different && $has_taxes) {
|
||||
return array(array(
|
||||
'id' => 'tax_subtotal',
|
||||
'title' => t('Subtotal excluding taxes'),
|
||||
'amount' => $amount,
|
||||
'weight' => variable_get('uc_li_tax_subtotal_weight', 7),
|
||||
));
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves a tax rate to the database.
|
||||
*
|
||||
* @param $rate
|
||||
* The tax rate object to be saved.
|
||||
* @param $reset
|
||||
* If TRUE, resets the Rules cache after saving. Defaults to TRUE.
|
||||
*
|
||||
* @return
|
||||
* The saved tax rate object including the rate ID for new rates.
|
||||
*/
|
||||
function uc_taxes_rate_save($rate, $reset = TRUE) {
|
||||
// Save it as a new rate if no ID is specified.
|
||||
if (empty($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'));
|
||||
}
|
||||
|
||||
db_delete('uc_taxed_product_types')
|
||||
->condition('tax_id', $rate->id)
|
||||
->execute();
|
||||
db_delete('uc_taxed_line_items')
|
||||
->condition('tax_id', $rate->id)
|
||||
->execute();
|
||||
|
||||
$p_insert = db_insert('uc_taxed_product_types')->fields(array('tax_id', 'type'));
|
||||
$l_insert = db_insert('uc_taxed_line_items')->fields(array('tax_id', 'type'));
|
||||
|
||||
foreach ($rate->taxed_product_types as $type) {
|
||||
$p_insert->values(array(
|
||||
'tax_id' => $rate->id,
|
||||
'type' => $type,
|
||||
));
|
||||
}
|
||||
|
||||
foreach ($rate->taxed_line_items as $type) {
|
||||
$l_insert->values(array(
|
||||
'tax_id' => $rate->id,
|
||||
'type' => $type,
|
||||
));
|
||||
}
|
||||
|
||||
$p_insert->execute();
|
||||
$l_insert->execute();
|
||||
|
||||
if ($reset) {
|
||||
// Ensure Rules picks up the new condition.
|
||||
entity_flush_caches();
|
||||
}
|
||||
|
||||
return $rate;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* List all the taxes that can apply to an order.
|
||||
*
|
||||
* The taxes depend on the order status. For orders which are still in
|
||||
* checkout, any tax can apply. For orders out of checkout, only taxes
|
||||
* originally saved as line items can apply.
|
||||
*
|
||||
* @param $order
|
||||
* The order that taxes are being calculated for.
|
||||
*/
|
||||
function uc_taxes_filter_rates($order) {
|
||||
$taxes = array();
|
||||
|
||||
// If no order, then just return all rates.
|
||||
if (empty($order)) {
|
||||
$taxes = uc_taxes_rate_load();
|
||||
}
|
||||
// For orders no longer in checkout, only the saved tax rates can apply.
|
||||
elseif (isset($order->order_status) && uc_order_status_data($order->order_status, 'state') != 'in_checkout') {
|
||||
if (isset($order->line_items)) {
|
||||
foreach ($order->line_items as $item) {
|
||||
if ($item['type'] == 'tax') {
|
||||
if (!empty($item['data']['tax'])) {
|
||||
// Use the rate stored in the line-item.
|
||||
$taxes[] = clone $item['data']['tax'];
|
||||
}
|
||||
elseif (!empty($item['data']['tax_id']) && $tax = uc_taxes_rate_load($item['data']['tax_id'])) {
|
||||
// For old orders that don't have all the tax info, all we can do
|
||||
// is preserve the rate.
|
||||
$tax = clone $tax;
|
||||
if (!empty($item['data']['tax_rate'])) {
|
||||
$tax->rate = $item['data']['tax_rate'];
|
||||
}
|
||||
$taxes[] = $tax;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// For orders still in checkout, any tax whose conditions are satisfied can
|
||||
// apply.
|
||||
else {
|
||||
foreach (uc_taxes_rate_load() as $rate) {
|
||||
$tax = clone $rate;
|
||||
if (rules_invoke_component('uc_taxes_' . $tax->id, $order)) {
|
||||
$taxes[] = $tax;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $taxes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
foreach ($result as $rate) {
|
||||
$rate->taxed_product_types = array();
|
||||
$rate->taxed_line_items = array();
|
||||
|
||||
$rates[$rate->id] = $rate;
|
||||
}
|
||||
|
||||
foreach (array('taxed_product_types', 'taxed_line_items') as $field) {
|
||||
$result = db_select('uc_' . $field, 't')->fields('t', array('tax_id', 'type'))->execute();
|
||||
foreach ($result as $record) {
|
||||
$rates[$record->tax_id]->{$field}[] = $record->type;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return a rate as specified.
|
||||
if ($rate_id) {
|
||||
return isset($rates[$rate_id]) ? $rates[$rate_id] : FALSE;
|
||||
}
|
||||
// Or return the whole shebang.
|
||||
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_delete('uc_taxes')
|
||||
->condition('id', $rate_id)
|
||||
->execute();
|
||||
|
||||
db_delete('uc_taxed_product_types')
|
||||
->condition('tax_id', $rate_id)
|
||||
->execute();
|
||||
|
||||
db_delete('uc_taxed_line_items')
|
||||
->condition('tax_id', $rate_id)
|
||||
->execute();
|
||||
|
||||
// Delete the associated conditions if they have been saved to the database.
|
||||
rules_config_delete(array('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('uc_calculate_tax', $order);
|
||||
|
||||
return $taxes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the amount and types of taxes that apply to an order.
|
||||
*/
|
||||
function uc_taxes_uc_calculate_tax($order) {
|
||||
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();
|
||||
foreach (uc_taxes_filter_rates($order) as $tax) {
|
||||
if ($line_item = uc_taxes_apply_tax($order, $tax)) {
|
||||
$order->taxes[$line_item->id] = $line_item;
|
||||
}
|
||||
}
|
||||
|
||||
return $order->taxes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates taxable amount for a single product.
|
||||
*/
|
||||
function uc_taxes_apply_item_tax($item, $tax) {
|
||||
// Determine the product type.
|
||||
if (is_array($item->data) && isset($item->data['type'])) { // Saved in the order product data array.
|
||||
$type = $item->data['type'];
|
||||
}
|
||||
elseif (empty($item->nid)) { // "Blank-line" product.
|
||||
$type = 'blank-line';
|
||||
}
|
||||
elseif ($node = node_load($item->nid)) { // Use type of current node, if it exists.
|
||||
$type = $node->type;
|
||||
}
|
||||
else { // Default to generic product.
|
||||
$type = 'product';
|
||||
}
|
||||
|
||||
// Determine whether this is a shippable product.
|
||||
if (is_array($item->data) && isset($item->data['shippable'])) { // Saved in the order product data array.
|
||||
$shippable = $item->data['shippable'];
|
||||
}
|
||||
elseif (empty($item->nid)) { // "Blank line" product.
|
||||
$shippable = $item->weight > 0;
|
||||
}
|
||||
elseif ($node = node_load($item->nid)) { // Use current node.
|
||||
$shippable = $node->shippable;
|
||||
}
|
||||
else {
|
||||
$shippable = variable_get('uc_product_shippable_' . $type); // Use default for this node type.
|
||||
}
|
||||
|
||||
// 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($type, $tax->taxed_product_types) && ($tax->shippable == 0 || $shippable == 1)) {
|
||||
return $item->price;
|
||||
}
|
||||
else {
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 += $item->qty * 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 = _uc_line_item_data($line_item['type'], 'tax_adjustment');
|
||||
if (isset($callback) && function_exists($callback)) {
|
||||
$taxable_amount += $callback($line_item['amount'], $order, $tax);
|
||||
}
|
||||
else {
|
||||
$taxable_amount += $line_item['amount'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (in_array('tax', $taxed_line_items)) {
|
||||
// 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_rate' => $tax->rate,
|
||||
'tax' => $tax,
|
||||
'taxable_amount' => $taxable_amount,
|
||||
'tax_jurisdiction' => $tax->name,
|
||||
);
|
||||
return $line_item;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the taxes that should be included in a product's display price.
|
||||
*
|
||||
* @param $product
|
||||
* The product whose included taxes are to be calculated.
|
||||
*/
|
||||
function uc_taxes_get_included_tax($product, $order = NULL) {
|
||||
$amount = 0;
|
||||
$suffixes = array();
|
||||
foreach (uc_taxes_filter_rates($order) as $tax) {
|
||||
if ($tax->display_include) {
|
||||
$taxable = uc_taxes_apply_item_tax($product, $tax);
|
||||
if (!empty($taxable)) {
|
||||
$amount += $taxable * $tax->rate;
|
||||
$suffixes[$tax->inclusion_text] = $tax->inclusion_text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return array($amount, $suffixes);
|
||||
}
|
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* This file contains the default Rules configurations that allow conditions to
|
||||
* be applied to taxes.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Implements hook_default_rules_configuration().
|
||||
*
|
||||
* Creates a condition set for each tax rule.
|
||||
*/
|
||||
function uc_taxes_default_rules_configuration() {
|
||||
$configs = array();
|
||||
|
||||
// Loop through all the defined tax rates.
|
||||
foreach (uc_taxes_rate_load() as $rate) {
|
||||
$set = rules_and(array(
|
||||
'order' => array('type' => 'uc_order', 'label' => 'Order'),
|
||||
));
|
||||
$set->label = t('@name conditions', array('@name' => $rate->name));
|
||||
|
||||
$configs['uc_taxes_' . $rate->id] = $set;
|
||||
}
|
||||
|
||||
return $configs;
|
||||
}
|
Reference in New Issue
Block a user